Files
lingniu-vehicle-ingest/go/vehicle-gateway/cmd/stats-backfill/protected_write_mysql_test.go
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

133 lines
5.6 KiB
Go

package main
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"os"
"reflect"
"testing"
"time"
mysql "github.com/go-sql-driver/mysql"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
// A lost session must never reconnect without its temporary shadow tables.
type oneSessionConnector struct {
driver.Connector
used bool
}
func (c *oneSessionConnector) Connect(ctx context.Context) (driver.Conn, error) {
if c.used {
return nil, errors.New("temporary-table integration session cannot reconnect")
}
c.used = true
return c.Connector.Connect(ctx)
}
func TestProtectedGPSWriteMySQLTemporaryTables(t *testing.T) {
dsn := os.Getenv("BACKFILL_TEST_MYSQL_DSN")
if dsn == "" {
t.Skip("set BACKFILL_TEST_MYSQL_DSN for isolated temporary-table integration")
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
t.Fatal("invalid integration DSN")
}
cfg.ParseTime = true
cfg.Loc = time.FixedZone("Asia/Shanghai", 8*3600)
connector, err := mysql.NewConnector(cfg)
if err != nil {
t.Fatal("integration connector unavailable")
}
db := sql.OpenDB(&oneSessionConnector{Connector: connector})
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
defer db.Close()
ctx := context.Background()
for _, table := range []string{"vehicle_daily_mileage_source", "vehicle_daily_mileage", "vehicle_data_source"} {
if _, err := db.ExecContext(ctx, "CREATE TEMPORARY TABLE gps_fixture_"+table+" LIKE "+table); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, "CREATE TEMPORARY TABLE "+table+" LIKE gps_fixture_"+table); err != nil {
t.Fatal(err)
}
}
loc := cfg.Loc
prev := time.Date(2026, 10, 20, 23, 59, 0, 0, loc)
end := time.Date(2026, 10, 21, 23, 59, 0, 0, loc)
const vin = "LTESTGPSPROTECT001"
key := "JT808:fixture@PLATFORM:g7s"
if _, err := db.Exec(`INSERT INTO vehicle_daily_mileage_source
(vin,stat_date,protocol,source_key,source_ip,source_endpoint,phone,platform_name,first_total_mileage_km,latest_total_mileage_km,daily_mileage_km,sample_count,first_event_time,latest_event_time,quality_status,quality_reason)
VALUES (?,'2026-10-21','JT808',?,'1.2.3.4','1.2.3.4:1','fixture','G7s',100,120,20,200,?,?,'OK','historical_source_baseline'),
(?,'2026-10-21','JT808','JT808:other@PLATFORM:other','2.3.4.5','2.3.4.5:2','other','Other',100,101,1,3,?,?,'OK','historical_source_baseline')`, vin, key, prev, end, vin, prev, end.Add(-time.Minute)); err != nil {
t.Fatal(err)
}
// Keep the existing manual final selected. This exercises the protected
// write path without MySQL's temporary-table self-join restriction in the
// independent projection query (covered separately by replay verification).
if _, err := db.Exec(`INSERT INTO vehicle_daily_mileage_source (vin,stat_date,protocol,source_key,source_ip,daily_mileage_km,sample_count,quality_status,quality_reason,is_selected)
VALUES (?,'2026-10-21','JT808','manual-report','manual-import',88,1,'OK','manual_gps_report',1)`, vin); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO vehicle_daily_mileage (vin,stat_date,protocol,source_id,daily_mileage_km) VALUES (?,'2026-10-21','JT808',0,88)`, vin); err != nil {
t.Fatal(err)
}
agg := &metricAgg{VIN: vin, Date: "2026-10-21", Protocol: envelope.ProtocolJT808, SourceKey: key, Phone: "fixture", SourceEndpoint: "1.2.3.4:3", SourceCode: "g7s", SourceKind: "PLATFORM", PlatformName: "G7s", FirstKM: 90, LatestKM: 115, Count: 100, FirstEventTime: prev.Add(-24 * time.Hour), LatestEventTime: end.Add(-time.Hour), QualityStatus: "OK", QualityReason: "historical_source_baseline"}
read := func() []any {
var first, last, daily float64
var count int
var firstTS, lastTS time.Time
if err := db.QueryRow(`SELECT first_total_mileage_km,latest_total_mileage_km,daily_mileage_km,sample_count,first_event_time,latest_event_time FROM vehicle_daily_mileage_source WHERE vin=? AND source_key=?`, vin, key).Scan(&first, &last, &daily, &count, &firstTS, &lastTS); err != nil {
t.Fatal(err)
}
return []any{first, last, daily, count, firstTS, lastTS}
}
var before []any
for run := 0; run < 2; run++ {
if _, err := writeAggregates(ctx, db, map[string]*metricAgg{"fixture": agg}, 500); err != nil {
t.Fatal(err)
}
got := read()
if got[0] != float64(100) || got[1] != float64(120) || got[2] != float64(20) || got[3] != 200 {
t.Fatalf("durable data overwritten: %+v", got)
}
if run == 1 && !reflect.DeepEqual(before, got) {
t.Fatalf("repeated backfill changed business values: before=%+v after=%+v", before, got)
}
before = got
}
var sources int
if err := db.QueryRow("SELECT COUNT(*) FROM vehicle_daily_mileage_source WHERE vin=?", vin).Scan(&sources); err != nil {
t.Fatal(err)
}
if sources != 3 {
t.Fatalf("missing durable alternate source: %d", sources)
}
// Newer event with a lower counter wins by time, not MAX(odometer).
agg.FirstKM, agg.FirstEventTime = 100, prev
agg.LatestKM, agg.LatestEventTime = 119, end.Add(time.Second)
if _, err := writeAggregates(ctx, db, map[string]*metricAgg{"fixture": agg}, 500); err != nil {
t.Fatal(err)
}
if got := read(); got[1] != float64(119) || got[2] != float64(19) {
t.Fatalf("newer event rejected: %+v", got)
}
agg.LatestKM, agg.LatestEventTime = 121, end.Add(2*time.Second)
if _, err := writeAggregates(ctx, db, map[string]*metricAgg{"fixture": agg}, 500); err != nil {
t.Fatal(err)
}
var final float64
if err := db.QueryRow("SELECT daily_mileage_km FROM vehicle_daily_mileage WHERE vin=?", vin).Scan(&final); err != nil {
t.Fatal(err)
}
if final != 88 {
t.Fatalf("manual final overwritten: %v", final)
}
t.Log("temporary-table scenarios passed: incomplete RAW, alternate source retention, repeated backfill, event ordering, manual final; production rows modified=0")
}