功能:完善里程边界与氢耗流式统计
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -189,7 +190,7 @@ func TestDailySourceLastBuildsCandidateKeysBySourceIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateFromDailySourceUsesOlderHistoricalBaseline(t *testing.T) {
|
||||
func TestAggregateFromDailySourceUsesCurrentDayBoundaryAfterOfflineGap(t *testing.T) {
|
||||
current := dailySourceLast{
|
||||
VIN: "LMRKH9AC2R1004087",
|
||||
SourceKey: normalizedSourceKey("YUTONG_MQTT", "", "LMRKH9AC2R1004087", "mqtt://yutong/ytforward/shln/3"),
|
||||
@@ -212,13 +213,13 @@ func TestAggregateFromDailySourceUsesOlderHistoricalBaseline(t *testing.T) {
|
||||
|
||||
agg := aggregateFromDailySource("2026-07-08", envelope.ProtocolYutongMQTT, current, previous, true)
|
||||
|
||||
if agg.FirstKM != 120672 || agg.LatestKM != 120788 {
|
||||
if agg.FirstKM != 120778 || agg.LatestKM != 120788 {
|
||||
t.Fatalf("km range = %v -> %v", agg.FirstKM, agg.LatestKM)
|
||||
}
|
||||
if agg.FirstEventTime != previous.TS || agg.LatestEventTime != current.TS {
|
||||
if agg.FirstEventTime != current.FirstTS || agg.LatestEventTime != current.TS {
|
||||
t.Fatalf("event range = %v -> %v", agg.FirstEventTime, agg.LatestEventTime)
|
||||
}
|
||||
if agg.QualityStatus != stats.QualityOK || agg.QualityReason != stats.QualityReasonHistorical {
|
||||
if agg.QualityStatus != stats.QualityOK || agg.QualityReason != stats.QualityReasonCurrentDayFirst {
|
||||
t.Fatalf("quality = %s/%s", agg.QualityStatus, agg.QualityReason)
|
||||
}
|
||||
if agg.Count != 15 {
|
||||
@@ -226,7 +227,7 @@ func TestAggregateFromDailySourceUsesOlderHistoricalBaseline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateFromDailySourceRejectsHistoricalBaselineJump(t *testing.T) {
|
||||
func TestAggregateFromDailySourceIgnoresHistoricalBaselineJumpAcrossOfflineGap(t *testing.T) {
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
current := dailySourceLast{
|
||||
VIN: "LNXNEGRR6SR319464",
|
||||
@@ -248,10 +249,10 @@ func TestAggregateFromDailySourceRejectsHistoricalBaselineJump(t *testing.T) {
|
||||
|
||||
agg := aggregateFromDailySource("2026-07-12", envelope.ProtocolGB32960, current, previous, true)
|
||||
|
||||
if agg.FirstKM != previous.TotalKM || agg.LatestKM != current.TotalKM {
|
||||
if agg.FirstKM != current.FirstTotalKM || agg.LatestKM != current.TotalKM {
|
||||
t.Fatalf("km range = %v -> %v", agg.FirstKM, agg.LatestKM)
|
||||
}
|
||||
if !agg.FirstEventTime.Equal(previous.TS) || !agg.LatestEventTime.Equal(current.TS) {
|
||||
if !agg.FirstEventTime.Equal(current.FirstTS) || !agg.LatestEventTime.Equal(current.TS) {
|
||||
t.Fatalf("event range = %v -> %v", agg.FirstEventTime, agg.LatestEventTime)
|
||||
}
|
||||
if agg.QualityStatus != stats.QualityInvalidDelta || agg.QualityReason != "outside_daily_range" {
|
||||
@@ -285,7 +286,7 @@ func TestAggregateFromDailySourceUsesCurrentDayFirstWhenHistoryIsEmpty(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLastDiffAggregatesCarriesNearestHistoryAcrossEmptyDays(t *testing.T) {
|
||||
func TestBuildLastDiffAggregatesStartsFreshBoundaryAfterEmptyDays(t *testing.T) {
|
||||
tdDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("sqlmock.New() error = %v", err)
|
||||
@@ -333,11 +334,11 @@ func TestBuildLastDiffAggregatesCarriesNearestHistoryAcrossEmptyDays(t *testing.
|
||||
if agg == nil {
|
||||
t.Fatalf("missing day-three aggregate; keys=%v", aggregateKeys(aggregates))
|
||||
}
|
||||
if agg.FirstKM != 100 || agg.LatestKM != 120 {
|
||||
t.Fatalf("km range = %v -> %v, want 100 -> 120", agg.FirstKM, agg.LatestKM)
|
||||
if agg.FirstKM != 120 || agg.LatestKM != 120 {
|
||||
t.Fatalf("km range = %v -> %v, want 120 -> 120", agg.FirstKM, agg.LatestKM)
|
||||
}
|
||||
if !agg.FirstEventTime.Equal(dayOneTS) || agg.QualityReason != stats.QualityReasonHistorical {
|
||||
t.Fatalf("baseline = %v reason=%q, want day-one historical baseline", agg.FirstEventTime, agg.QualityReason)
|
||||
if !agg.FirstEventTime.Equal(dayThreeTS) || agg.QualityReason != stats.QualityReasonCurrentDayFirst {
|
||||
t.Fatalf("baseline = %v reason=%q, want day-three current-day baseline", agg.FirstEventTime, agg.QualityReason)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations: %v", err)
|
||||
@@ -601,10 +602,10 @@ func TestAddRealtimeLocationFallbackReusesAggregateHistoryAcrossEmptyDays(t *tes
|
||||
if agg == nil {
|
||||
t.Fatal("missing realtime-location fallback aggregate")
|
||||
}
|
||||
if agg.FirstKM != 100 || agg.LatestKM != 120 || !agg.FirstEventTime.Equal(dayOneTS) {
|
||||
t.Fatalf("fallback range = %v@%v -> %v, want 100@day-one -> 120", agg.FirstKM, agg.FirstEventTime, agg.LatestKM)
|
||||
if agg.FirstKM != 120 || agg.LatestKM != 120 || !agg.FirstEventTime.Equal(dayThreeTS) {
|
||||
t.Fatalf("fallback range = %v@%v -> %v, want 120@day-three -> 120", agg.FirstKM, agg.FirstEventTime, agg.LatestKM)
|
||||
}
|
||||
if agg.QualityReason != "realtime_location_fallback_historical_baseline" {
|
||||
if agg.QualityReason != "realtime_location_fallback_current_day_first_baseline" {
|
||||
t.Fatalf("quality reason = %q", agg.QualityReason)
|
||||
}
|
||||
if err := mysqlMock.ExpectationsWereMet(); err != nil {
|
||||
@@ -823,6 +824,24 @@ func TestFieldsForStatsPromotesGB32960FuelCellEngineWorkState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldsForStatsPromotesGB32960VehicleRunningMode(t *testing.T) {
|
||||
fields := fieldsForStats(envelope.ProtocolGB32960, "LTEST000000000001", `{
|
||||
"gb32960.vehicle.total_mileage_km": "120.5",
|
||||
"gb32960.vehicle.running_mode": "1"
|
||||
}`)
|
||||
|
||||
if got := fields[envelope.FieldVehicleRunningMode]; got == nil {
|
||||
t.Fatalf("fields missing GB32960 vehicle running mode: %#v", fields)
|
||||
}
|
||||
active, known := stats.PureHydrogenModeFromEnvelope(envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolGB32960,
|
||||
Fields: fields,
|
||||
})
|
||||
if !known || active {
|
||||
t.Fatalf("pure-hydrogen mode = known:%v active:%v, want known inactive", known, active)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearBackfillTargetMileageClearsExactKey(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
@@ -1028,7 +1047,7 @@ func TestAddSamplesUsesCurrentDayFirstSampleBaseline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSamplesBackfillsOnlyContinuousPureHydrogenIntervals(t *testing.T) {
|
||||
func TestAddSamplesBackfillsTotalMileageMinusPureElectricIntervals(t *testing.T) {
|
||||
loc := time.FixedZone("Asia/Shanghai", 8*3600)
|
||||
base := stats.MetricSample{
|
||||
VIN: "LTEST000000000001",
|
||||
@@ -1061,8 +1080,8 @@ func TestAddSamplesBackfillsOnlyContinuousPureHydrogenIntervals(t *testing.T) {
|
||||
t.Fatalf("aggregate count = %d, want 1", len(aggregates))
|
||||
}
|
||||
for _, agg := range aggregates {
|
||||
if agg.PureHydrogenMileageKM != 5 {
|
||||
t.Fatalf("pure-hydrogen mileage = %v, want 5", agg.PureHydrogenMileageKM)
|
||||
if agg.PureHydrogenMileageKM != 7 {
|
||||
t.Fatalf("pure-hydrogen mileage = %v, want 7", agg.PureHydrogenMileageKM)
|
||||
}
|
||||
if agg.PureHydrogenSampleCount != 2 {
|
||||
t.Fatalf("pure-hydrogen sample count = %d, want 2", agg.PureHydrogenSampleCount)
|
||||
@@ -1094,6 +1113,14 @@ func TestLoadConfigDefaultsBackfillMethodToLastDiff(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVINsNormalizesAndDeduplicates(t *testing.T) {
|
||||
got := parseVINs(" la9gg68l1pbaf4814, LFV123 ,LA9GG68L1PBAF4814, ")
|
||||
want := []string{"LA9GG68L1PBAF4814", "LFV123"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parseVINs() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackfillTimePredicatesUsePrimaryTimeForCoarseScanAndEventTimeForBusinessDay(t *testing.T) {
|
||||
where := strings.Join(backfillTimePredicates(config{}, "2026-07-13", "2026-07-14"), " AND ")
|
||||
for _, want := range []string{
|
||||
|
||||
Reference in New Issue
Block a user