fix(stats): recover stale odometer mileage
This commit is contained in:
@@ -2109,6 +2109,78 @@ func TestWriterAppendWithResultReportsDuplicateMileage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterDuplicateYutongOdometerStillEntersGPSFallback(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
writer := NewWriter(db, loc)
|
||||
eventTime := time.Date(2026, 7, 19, 8, 0, 0, 0, loc)
|
||||
event := managedTestSource(envelope.FrameEnvelope{
|
||||
EventID: "evt-yutong-stale-odometer",
|
||||
Protocol: envelope.ProtocolYutongMQTT,
|
||||
VIN: "LMRKH9AC9R1004099",
|
||||
DeviceID: "LMRKH9AC9R1004099",
|
||||
SourceEndpoint: "mqtt://yutong/ytforward/shln/2",
|
||||
EventTimeMS: eventTime.UnixMilli(),
|
||||
Fields: map[string]any{
|
||||
"yutong_mqtt.data.total_mileage": 24245000,
|
||||
"yutong_mqtt.data.longitude": 114.435672,
|
||||
"yutong_mqtt.data.latitude": 30.645438,
|
||||
"yutong_mqtt.data.meter_speed": 18,
|
||||
},
|
||||
})
|
||||
samples, _, err := samplesFromEnvelopeWithResult(event, loc)
|
||||
if err != nil || len(samples) != 1 {
|
||||
t.Fatalf("samplesFromEnvelopeWithResult() samples=%+v err=%v", samples, err)
|
||||
}
|
||||
identity, ok := NewSourceIdentityFromEnvelope(event)
|
||||
if !ok {
|
||||
t.Fatal("expected managed Yutong source identity")
|
||||
}
|
||||
writer.markSourceTouched(identity, eventTime)
|
||||
writer.markMileageWritten(samples[0])
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT first_event_time, latest_event_time`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"first_event_time", "latest_event_time", "latest_event_id",
|
||||
"latest_longitude", "latest_latitude", "daily_mileage_km",
|
||||
"point_count", "usable_segment_count", "bad_jump_count",
|
||||
"long_gap_count", "out_of_order_count",
|
||||
}).AddRow(eventTime.Add(-20*time.Second), eventTime.Add(-20*time.Second), "evt-before", 114.434672, 30.645438, 0, 1, 0, 0, 0, 0))
|
||||
mock.ExpectExec(`UPDATE vehicle_daily_gps_mileage_state`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage_source`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`INSERT INTO vehicle_daily_mileage`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`UPDATE vehicle_daily_mileage_source`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec(`DELETE FROM vehicle_daily_mileage`).
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectCommit()
|
||||
|
||||
result, err := writer.AppendWithResult(context.Background(), event)
|
||||
if err != nil {
|
||||
t.Fatalf("AppendWithResult() error = %v", err)
|
||||
}
|
||||
if result.SamplesFound != 1 || result.SamplesSkippedSameMileage != 1 ||
|
||||
result.SamplesRecoveredGPSCoordinate != 1 || result.SamplesWritten != 1 ||
|
||||
result.ProjectionsAttempted != 1 || result.ProjectionsWritten != 1 {
|
||||
t.Fatalf("duplicate odometer evidence = %+v", result)
|
||||
}
|
||||
if got := writer.CacheStats().GPSAccumulationEntries; got != 1 {
|
||||
t.Fatalf("GPS accumulation entries = %d, want stale moving odometer to enter fallback", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet SQL expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterDoesNotCacheMileageWhenUpsertFails(t *testing.T) {
|
||||
exec := &recordingExec{errs: []error{errors.New("mysql unavailable"), nil}}
|
||||
writer := NewWriter(exec, time.FixedZone("Asia/Shanghai", 8*3600))
|
||||
|
||||
Reference in New Issue
Block a user