fix(monitor): arbitrate conflicting location sources

This commit is contained in:
lingniu
2026-07-16 11:05:28 +08:00
parent 9384d6acf5
commit b9fcf476ec
11 changed files with 360 additions and 44 deletions

View File

@@ -5,6 +5,8 @@ import (
"database/sql"
"encoding/json"
"errors"
"math"
"net"
"strings"
"sync"
"time"
@@ -179,6 +181,12 @@ func (w *SnapshotWriter) EnsureSchema(ctx context.Context) error {
return err
}
}
if _, err := w.exec.ExecContext(ctx, jt808RealtimeLocationSourceTableSQL); err != nil {
return err
}
if _, err := w.exec.ExecContext(ctx, vehicleLocationSourcePolicyTableSQL); err != nil {
return err
}
for _, statement := range cleanupInvalidRealtimeTotalMileageSQL {
if _, err := w.exec.ExecContext(ctx, statement); err != nil {
return err
@@ -230,7 +238,7 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
if !ok {
return nil
}
_, err = w.exec.ExecContext(ctx, upsertRealtimeLocationSQL,
locationArgs := []any{
location.Protocol,
location.VIN,
location.Plate,
@@ -247,10 +255,66 @@ func (w *SnapshotWriter) Update(ctx context.Context, env envelope.FrameEnvelope)
location.StatusFlag,
location.ReceivedAt,
location.EventID,
)
}
if env.Protocol != envelope.ProtocolJT808 {
_, err = w.exec.ExecContext(ctx, upsertRealtimeLocationSQL, locationArgs...)
return err
}
sourceKey := realtimeLocationSourceKey(env, vin)
sourceArgs := append([]any{sourceKey, strings.TrimSpace(env.Phone), strings.TrimSpace(env.DeviceID), strings.TrimSpace(env.SourceCode), normalizedSourceKind(env.SourceKind), strings.TrimSpace(env.SourceEndpoint)}, locationArgs...)
sourceArgs = append(sourceArgs, location.ReceivedAt)
if _, err = w.exec.ExecContext(ctx, upsertJT808RealtimeLocationSourceSQL, sourceArgs...); err != nil {
return err
}
_, err = w.exec.ExecContext(ctx, electJT808RealtimeLocationSQL, vin)
return err
}
func realtimeLocationSourceKey(env envelope.FrameEnvelope, vin string) string {
identity := strings.TrimSpace(env.Phone)
if identity == "" {
identity = strings.TrimSpace(env.DeviceID)
}
if identity == "" {
identity = strings.TrimSpace(vin)
}
scope := strings.TrimSpace(env.SourceCode)
if scope == "" {
scope = normalizedSourceKind(env.SourceKind)
}
if scope == "" || scope == "UNKNOWN" {
scope = sourceHost(env.SourceEndpoint)
}
if scope == "" {
scope = "unknown"
}
return string(env.Protocol) + ":" + identity + "@" + scope
}
func normalizedSourceKind(value string) string {
value = strings.ToUpper(strings.TrimSpace(value))
switch value {
case "DIRECT", "PLATFORM":
return value
default:
return "UNKNOWN"
}
}
func sourceHost(endpoint string) string {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return ""
}
if host, _, err := net.SplitHostPort(endpoint); err == nil {
return strings.TrimSpace(host)
}
if index := strings.LastIndex(endpoint, ":"); index > 0 && strings.Count(endpoint, ":") == 1 {
return endpoint[:index]
}
return endpoint
}
func snapshotFieldsForEnvelope(env envelope.FrameEnvelope) map[string]any {
if len(env.ParsedFields) == 0 {
return nil
@@ -347,6 +411,11 @@ func realtimeLocationFromEnvelope(env envelope.FrameEnvelope, vin string, plate
if !ok {
return realtimeLocationRow{}, false
}
if math.IsNaN(location.Latitude) || math.IsNaN(location.Longitude) || math.IsInf(location.Latitude, 0) || math.IsInf(location.Longitude, 0) ||
location.Latitude < -90 || location.Latitude > 90 || location.Longitude < -180 || location.Longitude > 180 ||
(math.Abs(location.Latitude) < 0.000001 && math.Abs(location.Longitude) < 0.000001) {
return realtimeLocationRow{}, false
}
eventTimeMS, _ := envelope.NormalizedEventTimeMS(env)
totalMileageKM, hasTotalMileage := telemetry.TotalMileageKM(env.Protocol, env.ParsedFields)
if totalMileageKM <= 0 {
@@ -554,8 +623,137 @@ const realtimeLocationTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_lo
var realtimeLocationCompatibilitySQL = []string{
"ALTER TABLE vehicle_realtime_location ADD COLUMN total_mileage_event_time DATETIME(3) NULL AFTER total_mileage_km",
"ALTER TABLE vehicle_realtime_location ADD COLUMN source_key VARCHAR(256) NOT NULL DEFAULT '' AFTER plate",
"ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict TINYINT(1) NOT NULL DEFAULT 0 AFTER status_flag",
"ALTER TABLE vehicle_realtime_location ADD COLUMN location_conflict_distance_m DECIMAL(12,1) NULL AFTER location_conflict",
}
const jt808RealtimeLocationSourceTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_realtime_location_source (
protocol VARCHAR(32) NOT NULL,
vin VARCHAR(32) NOT NULL,
source_key VARCHAR(256) NOT NULL,
phone VARCHAR(32) NOT NULL DEFAULT '',
device_id VARCHAR(64) NOT NULL DEFAULT '',
source_code VARCHAR(64) NOT NULL DEFAULT '',
source_kind VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN',
source_endpoint VARCHAR(128) NOT NULL DEFAULT '',
plate VARCHAR(32) NOT NULL DEFAULT '',
event_time DATETIME(3) NULL,
latitude DECIMAL(12,6) NOT NULL,
longitude DECIMAL(12,6) NOT NULL,
speed_kmh DECIMAL(10,3) NULL,
total_mileage_km DECIMAL(18,3) NULL,
total_mileage_event_time DATETIME(3) NULL,
soc_percent DECIMAL(6,2) NULL,
altitude_m DECIMAL(10,3) NULL,
direction_deg DECIMAL(10,3) NULL,
alarm_flag BIGINT NULL,
status_flag BIGINT NULL,
received_at DATETIME(3) NULL,
event_id VARCHAR(64) NOT NULL DEFAULT '',
quality_status VARCHAR(32) NOT NULL DEFAULT 'OK',
quality_reason VARCHAR(64) NOT NULL DEFAULT '',
consecutive_good_samples INT NOT NULL DEFAULT 1,
latest_sample_received_at DATETIME(3) NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (protocol, vin, source_key),
KEY idx_location_source_vehicle_fresh (vin, protocol, received_at),
KEY idx_location_source_quality (quality_status, updated_at)
)`
const vehicleLocationSourcePolicyTableSQL = `CREATE TABLE IF NOT EXISTS vehicle_location_source_policy (
vin VARCHAR(32) NOT NULL,
protocol VARCHAR(32) NOT NULL,
source_key VARCHAR(256) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
priority INT NOT NULL DEFAULT 100,
remark VARCHAR(255) NOT NULL DEFAULT '',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (vin, protocol, source_key),
KEY idx_location_policy_enabled (vin, protocol, enabled, priority)
)`
const jt808AcceptedLocationCondition = `VALUES(event_time) IS NOT NULL
AND (vehicle_realtime_location_source.event_time IS NULL OR VALUES(event_time) >= vehicle_realtime_location_source.event_time)
AND (vehicle_realtime_location_source.event_time IS NULL
OR ST_Distance_Sphere(
POINT(vehicle_realtime_location_source.longitude, vehicle_realtime_location_source.latitude),
POINT(VALUES(longitude), VALUES(latitude))
) <= GREATEST(500, GREATEST(1, TIMESTAMPDIFF(SECOND, vehicle_realtime_location_source.event_time, VALUES(event_time))) * 70))`
var upsertJT808RealtimeLocationSourceSQL = `
INSERT INTO vehicle_realtime_location_source
(source_key, phone, device_id, source_code, source_kind, source_endpoint,
protocol, vin, plate, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time,
soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, received_at, event_id, latest_sample_received_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
quality_status = IF(` + jt808AcceptedLocationCondition + `, 'OK', 'REJECTED'),
quality_reason = IF(quality_status = 'OK', '', 'impossible_jump'),
consecutive_good_samples = IF(quality_status = 'OK', IF(VALUES(event_id) <> event_id, consecutive_good_samples + 1, consecutive_good_samples), 0),
latest_sample_received_at = VALUES(latest_sample_received_at),
phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
device_id = IF(VALUES(device_id) <> '', VALUES(device_id), device_id),
source_code = IF(VALUES(source_code) <> '', VALUES(source_code), source_code),
source_kind = IF(VALUES(source_kind) <> 'UNKNOWN', VALUES(source_kind), source_kind),
source_endpoint = IF(VALUES(source_endpoint) <> '', VALUES(source_endpoint), source_endpoint),
plate = IF(VALUES(plate) <> '', VALUES(plate), plate),
event_time = IF(quality_status = 'OK', VALUES(event_time), event_time),
latitude = IF(quality_status = 'OK', VALUES(latitude), latitude),
longitude = IF(quality_status = 'OK', VALUES(longitude), longitude),
speed_kmh = IF(quality_status = 'OK', COALESCE(VALUES(speed_kmh), speed_kmh), speed_kmh),
total_mileage_km = IF(quality_status = 'OK', COALESCE(VALUES(total_mileage_km), total_mileage_km), total_mileage_km),
total_mileage_event_time = IF(quality_status = 'OK' AND VALUES(total_mileage_km) IS NOT NULL, COALESCE(VALUES(total_mileage_event_time), total_mileage_event_time), total_mileage_event_time),
soc_percent = IF(quality_status = 'OK', COALESCE(VALUES(soc_percent), soc_percent), soc_percent),
altitude_m = IF(quality_status = 'OK', COALESCE(VALUES(altitude_m), altitude_m), altitude_m),
direction_deg = IF(quality_status = 'OK', COALESCE(VALUES(direction_deg), direction_deg), direction_deg),
alarm_flag = IF(quality_status = 'OK', COALESCE(VALUES(alarm_flag), alarm_flag), alarm_flag),
status_flag = IF(quality_status = 'OK', COALESCE(VALUES(status_flag), status_flag), status_flag),
received_at = IF(quality_status = 'OK', VALUES(received_at), received_at),
event_id = IF(quality_status = 'OK', VALUES(event_id), event_id)
`
const electJT808RealtimeLocationSQL = `
INSERT INTO vehicle_realtime_location
(protocol, vin, plate, source_key, event_time, latitude, longitude, speed_kmh, total_mileage_km, total_mileage_event_time,
soc_percent, altitude_m, direction_deg, alarm_flag, status_flag, location_conflict, location_conflict_distance_m,
received_at, event_id)
SELECT s.protocol, s.vin, s.plate, s.source_key, s.event_time, s.latitude, s.longitude, s.speed_kmh,
s.total_mileage_km, s.total_mileage_event_time, s.soc_percent, s.altitude_m, s.direction_deg,
s.alarm_flag, s.status_flag,
EXISTS(SELECT 1 FROM vehicle_realtime_location_source other
WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key
AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
AND ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude)) > 200),
(SELECT MAX(ST_Distance_Sphere(POINT(s.longitude, s.latitude), POINT(other.longitude, other.latitude)))
FROM vehicle_realtime_location_source other
WHERE other.vin = s.vin AND other.protocol = s.protocol AND other.source_key <> s.source_key
AND other.quality_status = 'OK' AND other.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)),
s.received_at, s.event_id
FROM vehicle_realtime_location_source s
LEFT JOIN vehicle_location_source_policy policy
ON policy.vin = s.vin AND policy.protocol = s.protocol AND policy.source_key = s.source_key
LEFT JOIN vehicle_realtime_location cur
ON cur.vin = s.vin AND cur.protocol = s.protocol
WHERE s.vin = ? AND s.protocol = 'JT808' AND s.quality_status = 'OK' AND COALESCE(policy.enabled, 1) = 1
ORDER BY
CASE WHEN s.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN 0 ELSE 1 END ASC,
(COALESCE(policy.priority, CASE s.source_kind WHEN 'DIRECT' THEN 20 WHEN 'PLATFORM' THEN 30 ELSE 40 END)
+ CASE WHEN cur.source_key = s.source_key AND cur.received_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE) THEN -5 ELSE 0 END
+ CASE WHEN cur.source_key <> '' AND cur.source_key <> s.source_key AND s.consecutive_good_samples < 3 THEN 1000 ELSE 0 END) ASC,
s.received_at DESC, s.source_key ASC
LIMIT 1
ON DUPLICATE KEY UPDATE
plate = IF(VALUES(plate) <> '', VALUES(plate), vehicle_realtime_location.plate), source_key = VALUES(source_key),
event_time = VALUES(event_time), latitude = VALUES(latitude), longitude = VALUES(longitude),
speed_kmh = VALUES(speed_kmh), total_mileage_km = VALUES(total_mileage_km),
total_mileage_event_time = VALUES(total_mileage_event_time), soc_percent = VALUES(soc_percent),
altitude_m = VALUES(altitude_m), direction_deg = VALUES(direction_deg), alarm_flag = VALUES(alarm_flag),
status_flag = VALUES(status_flag), location_conflict = VALUES(location_conflict),
location_conflict_distance_m = VALUES(location_conflict_distance_m), received_at = VALUES(received_at),
event_id = VALUES(event_id), updated_at = CURRENT_TIMESTAMP
`
var cleanupInvalidRealtimeTotalMileageSQL = []string{
`UPDATE vehicle_realtime_location
SET total_mileage_km = NULL,

View File

@@ -46,8 +46,8 @@ func TestSnapshotWriterEnsuresSchemaAndUpsertsCoreSnapshot(t *testing.T) {
t.Fatalf("Update() error = %v", err)
}
if len(exec.calls) != 19 {
t.Fatalf("exec calls = %d, want 19", len(exec.calls))
if len(exec.calls) != 24 {
t.Fatalf("exec calls = %d, want 24", len(exec.calls))
}
if !strings.Contains(exec.calls[0].query, "CREATE TABLE IF NOT EXISTS vehicle_realtime_snapshot") {
t.Fatalf("schema query = %s", exec.calls[0].query)
@@ -84,15 +84,18 @@ func TestSnapshotWriterEnsuresSchemaAndUpsertsCoreSnapshot(t *testing.T) {
if !strings.Contains(exec.calls[12].query, "total_mileage_event_time") {
t.Fatalf("location compatibility migration should add total mileage event time: %s", exec.calls[12].query)
}
for _, call := range exec.calls[13:17] {
if !strings.Contains(exec.calls[16].query, "vehicle_realtime_location_source") || !strings.Contains(exec.calls[17].query, "vehicle_location_source_policy") {
t.Fatalf("source arbitration schema missing: %s / %s", exec.calls[16].query, exec.calls[17].query)
}
for _, call := range exec.calls[18:22] {
if !strings.Contains(call.query, "total_mileage") {
t.Fatalf("schema bootstrap should clean invalid realtime mileage: %s", call.query)
}
}
if !strings.Contains(exec.calls[17].query, "snapshot_backfill") {
t.Fatalf("access projection baseline should be backfilled explicitly: %s", exec.calls[17].query)
if !strings.Contains(exec.calls[22].query, "snapshot_backfill") {
t.Fatalf("access projection baseline should be backfilled explicitly: %s", exec.calls[22].query)
}
upsert := exec.calls[18]
upsert := exec.calls[23]
if !strings.Contains(upsert.query, "ON DUPLICATE KEY UPDATE") {
t.Fatalf("upsert query = %s", upsert.query)
}
@@ -150,6 +153,14 @@ func TestSnapshotWriterEnsureSchemaOnlyCreatesTargetTables(t *testing.T) {
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("ALTER TABLE vehicle_realtime_location ADD COLUMN total_mileage_event_time").
WillReturnResult(sqlmock.NewResult(0, 0))
for _, column := range []string{"source_key", "location_conflict", "location_conflict_distance_m"} {
mock.ExpectExec("ALTER TABLE vehicle_realtime_location ADD COLUMN " + column).
WillReturnResult(sqlmock.NewResult(0, 0))
}
mock.ExpectExec("CREATE TABLE IF NOT EXISTS vehicle_realtime_location_source").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("CREATE TABLE IF NOT EXISTS vehicle_location_source_policy").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("UPDATE vehicle_realtime_location").
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec("UPDATE vehicle_realtime_snapshot").
@@ -235,47 +246,97 @@ func TestSnapshotWriterUpsertsRealtimeLocationWhenCoordinatesExist(t *testing.T)
t.Fatalf("Update() error = %v", err)
}
if len(exec.calls) != 2 {
t.Fatalf("exec calls = %d, want 2", len(exec.calls))
if len(exec.calls) != 3 {
t.Fatalf("exec calls = %d, want 3", len(exec.calls))
}
locationUpsert := exec.calls[1]
if !strings.Contains(locationUpsert.query, "INSERT INTO vehicle_realtime_location") {
if !strings.Contains(locationUpsert.query, "INSERT INTO vehicle_realtime_location_source") {
t.Fatalf("location upsert query = %s", locationUpsert.query)
}
if strings.Contains(locationUpsert.query, "fields_json") {
t.Fatalf("location upsert should not write fields_json: %s", locationUpsert.query)
}
for _, column := range []string{"message_id", "sequence_id", "source_endpoint"} {
for _, column := range []string{"message_id", "sequence_id"} {
if strings.Contains(locationUpsert.query, column) {
t.Fatalf("location upsert should not write %s: %s", column, locationUpsert.query)
}
}
if got, want := locationUpsert.args[0], "JT808"; got != want {
if got, want := locationUpsert.args[6], "JT808"; got != want {
t.Fatalf("protocol arg = %#v, want %q", got, want)
}
if got, want := locationUpsert.args[1], "VIN001"; got != want {
if got, want := locationUpsert.args[7], "VIN001"; got != want {
t.Fatalf("vin arg = %#v, want %q", got, want)
}
if got, want := locationUpsert.args[4], 30.590151; got != want {
if got, want := locationUpsert.args[10], 30.590151; got != want {
t.Fatalf("latitude arg = %#v, want %v", got, want)
}
if got, want := locationUpsert.args[5], 121.069881; got != want {
if got, want := locationUpsert.args[11], 121.069881; got != want {
t.Fatalf("longitude arg = %#v, want %v", got, want)
}
if got, want := locationUpsert.args[6], 23.0; got != want {
if got, want := locationUpsert.args[12], 23.0; got != want {
t.Fatalf("speed arg = %#v, want %v", got, want)
}
if got, want := locationUpsert.args[7], 10241.2; got != want {
if got, want := locationUpsert.args[13], 10241.2; got != want {
t.Fatalf("mileage arg = %#v, want %v", got, want)
}
if _, ok := locationUpsert.args[8].(time.Time); !ok {
t.Fatalf("total mileage event time arg = %#v, want time.Time", locationUpsert.args[8])
if _, ok := locationUpsert.args[14].(time.Time); !ok {
t.Fatalf("total mileage event time arg = %#v, want time.Time", locationUpsert.args[14])
}
if got := locationUpsert.args[9]; got != nil {
if got := locationUpsert.args[15]; got != nil {
t.Fatalf("JT808 location should not invent SOC, got %#v", got)
}
if len(locationUpsert.args) != 16 {
t.Fatalf("location upsert args = %d, want 16", len(locationUpsert.args))
if len(locationUpsert.args) != 23 {
t.Fatalf("location source upsert args = %d, want 23", len(locationUpsert.args))
}
if got := locationUpsert.args[0]; got != "JT808:13307795425@1.2.3.4" {
t.Fatalf("stable source key = %#v", got)
}
if !strings.Contains(exec.calls[2].query, "cur.source_key") || !strings.Contains(exec.calls[2].query, "consecutive_good_samples < 3") {
t.Fatalf("canonical election must use sticky source hysteresis: %s", exec.calls[2].query)
}
}
func TestRealtimeLocationSourceKeyIsStableAcrossConnectionPorts(t *testing.T) {
platform := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "013307795425", SourceCode: "g7s", SourceKind: "PLATFORM", SourceEndpoint: "1.2.3.4:40001"}
if got := realtimeLocationSourceKey(platform, "VIN001"); got != "JT808:013307795425@g7s" {
t.Fatalf("platform source key = %q", got)
}
platform.SourceEndpoint = "1.2.3.4:49999"
if got := realtimeLocationSourceKey(platform, "VIN001"); got != "JT808:013307795425@g7s" {
t.Fatalf("reconnected platform source key = %q", got)
}
direct := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "013307795425", SourceKind: "DIRECT", SourceEndpoint: "5.6.7.8:50001"}
if got := realtimeLocationSourceKey(direct, "VIN001"); got != "JT808:013307795425@DIRECT" {
t.Fatalf("direct source key = %q", got)
}
}
func TestJT808LocationArbitrationGuardsJumpsAndSourceFlapping(t *testing.T) {
for _, want := range []string{"ST_Distance_Sphere", "GREATEST(500", "* 70", "impossible_jump", "consecutive_good_samples + 1"} {
if !strings.Contains(upsertJT808RealtimeLocationSourceSQL, want) {
t.Fatalf("JT808 source guard missing %q: %s", want, upsertJT808RealtimeLocationSourceSQL)
}
}
for _, want := range []string{"vehicle_location_source_policy", "cur.source_key = s.source_key", "consecutive_good_samples < 3", "INTERVAL 2 MINUTE", "> 200"} {
if !strings.Contains(electJT808RealtimeLocationSQL, want) {
t.Fatalf("JT808 canonical election missing %q: %s", want, electJT808RealtimeLocationSQL)
}
}
if !strings.Contains(electJT808RealtimeLocationSQL, "vehicle_realtime_location.plate") {
t.Fatalf("canonical upsert must qualify the target plate across joined source tables: %s", electJT808RealtimeLocationSQL)
}
}
func TestRealtimeLocationRejectsZeroCoordinate(t *testing.T) {
_, ok := realtimeLocationFromEnvelope(envelope.FrameEnvelope{
Protocol: envelope.ProtocolJT808,
ParsedFields: map[string]any{
"jt808.location.latitude": 0,
"jt808.location.longitude": 0,
},
}, "VIN001", "")
if ok {
t.Fatal("zero coordinate must not enter realtime location state")
}
}
@@ -299,7 +360,7 @@ func TestSnapshotWriterDropsNonPositiveTotalMileageFromRealtimeStores(t *testing
t.Fatalf("Update() error = %v", err)
}
if len(exec.calls) != 2 {
if len(exec.calls) != 3 {
t.Fatalf("exec calls = %d, want snapshot + location", len(exec.calls))
}
parsedJSON, ok := exec.calls[0].args[5].(string)
@@ -312,11 +373,11 @@ func TestSnapshotWriterDropsNonPositiveTotalMileageFromRealtimeStores(t *testing
if !strings.Contains(parsedJSON, "jt808.location.speed_kmh") {
t.Fatalf("snapshot parsed_json should keep valid fields: %s", parsedJSON)
}
if exec.calls[1].args[7] != nil {
t.Fatalf("location total mileage arg = %#v, want nil", exec.calls[1].args[7])
if exec.calls[1].args[13] != nil {
t.Fatalf("location total mileage arg = %#v, want nil", exec.calls[1].args[13])
}
if exec.calls[1].args[8] != nil {
t.Fatalf("location total mileage time arg = %#v, want nil", exec.calls[1].args[8])
if exec.calls[1].args[14] != nil {
t.Fatalf("location total mileage time arg = %#v, want nil", exec.calls[1].args[14])
}
}
@@ -342,17 +403,17 @@ func TestSnapshotWriterNormalizesFarFutureEventTime(t *testing.T) {
t.Fatalf("Update() error = %v", err)
}
if len(exec.calls) != 2 {
if len(exec.calls) != 3 {
t.Fatalf("exec calls = %d, want snapshot + location", len(exec.calls))
}
if got, ok := exec.calls[0].args[6].(time.Time); !ok || !got.Equal(received) {
t.Fatalf("snapshot event_time arg = %#v, want received %s", exec.calls[0].args[6], received)
}
if got, ok := exec.calls[1].args[3].(time.Time); !ok || !got.Equal(received) {
t.Fatalf("location event_time arg = %#v, want received %s", exec.calls[1].args[3], received)
if got, ok := exec.calls[1].args[9].(time.Time); !ok || !got.Equal(received) {
t.Fatalf("location event_time arg = %#v, want received %s", exec.calls[1].args[9], received)
}
if got, ok := exec.calls[1].args[8].(time.Time); !ok || !got.Equal(received) {
t.Fatalf("total mileage event time arg = %#v, want received %s", exec.calls[1].args[8], received)
if got, ok := exec.calls[1].args[14].(time.Time); !ok || !got.Equal(received) {
t.Fatalf("total mileage event time arg = %#v, want received %s", exec.calls[1].args[14], received)
}
}