package rond import ( "database/sql" "math" "os" "path/filepath" "runtime" "strings" "testing" "time" ) func nullF(v float64) sql.NullFloat64 { return sql.NullFloat64{Float64: v, Valid: true} } // 脏值必须判成「装不下」(nil)。旧实现直接 int64(sec),超范围是回绕不是饱和: // 实测 NaN/±Inf/1e30 恰好回绕到基准点(2001-01-01)、1e12 落到 2123 年。 func TestAppleTimeRejectsDirtyValues(t *testing.T) { dirty := map[string]float64{ "NaN": math.NaN(), "+Inf": math.Inf(1), "-Inf": math.Inf(-1), "1e30": 1e30, "-1e30": -1e30, "1e12": 1e12, // 会落到 2123 年 "-1e12": -1e12, "早于窗口": appleMinSec - 86400, "晚于窗口": appleMaxSec + 86400, } for name, v := range dirty { if tm := appleTime(nullF(v)); tm != nil { t.Errorf("%s 应当被拒(nil),实际得到 %s", name, tm.Format(time.RFC3339Nano)) } } if appleTime(sql.NullFloat64{}) != nil { t.Error("NULL 应当返回 nil") } } // 合法值要原样保留,尤其是微秒精度——整数/小数分开算就是为了它。 func TestAppleTimeKeepsLegitValues(t *testing.T) { for _, want := range []time.Time{ time.Date(1995, 7, 1, 12, 30, 45, 123456000, time.UTC), time.Date(2018, 3, 9, 21, 4, 5, 987654000, time.UTC), time.Date(2026, 9, 21, 10, 19, 9, 1000, time.UTC), } { got := appleTime(nullF(want.Sub(appleEpoch).Seconds())) if got == nil { t.Fatalf("%s 被误拒", want.Format(time.RFC3339Nano)) } if !got.Equal(want) { t.Errorf("时间失真: 期望 %s 得到 %s", want.Format(time.RFC3339Nano), got.Format(time.RFC3339Nano)) } } for _, edge := range []float64{appleMinSec, appleMaxSec} { if appleTime(nullF(edge)) == nil { t.Errorf("窗口边界 %.0f 不该被拒", edge) } } } func TestValidCoord(t *testing.T) { for _, c := range [][2]float64{ {math.NaN(), 0}, {0, math.NaN()}, {math.Inf(1), 0}, {0, math.Inf(-1)}, {91, 0}, {-91, 0}, {0, 181}, {0, -181}, {999, 999}, } { if validCoord(c[0], c[1]) { t.Errorf("无效坐标 (%v,%v) 被判成可用", c[0], c[1]) } } for _, c := range [][2]float64{{0, 0}, {24.1, 109.1}, {90, 180}, {-90, -180}, {39.9042, 116.4074}} { if !validCoord(c[0], c[1]) { t.Errorf("合法坐标 (%v,%v) 被误拒", c[0], c[1]) } } } // 函数再对,没接上去也白搭:读 ZLOCATION 的循环必须过一遍 validCoord。 func TestParseSanitizesLocationCoords(t *testing.T) { _, self, _, ok := runtime.Caller(0) if !ok { t.Fatal("拿不到测试文件路径") } src, err := os.ReadFile(filepath.Join(filepath.Dir(self), "parse.go")) if err != nil { t.Fatal(err) } text := string(src) i := strings.Index(text, "FROM ZLOCATION") if i < 0 { t.Fatal("parse.go 里找不到 ZLOCATION 查询") } j := strings.Index(text[i:], "b.Locations = append") if j < 0 { t.Fatal("找不到了 ZLOCATION 循环的收尾") } if !strings.Contains(text[i:i+j], "validCoord(") { t.Error("读 ZLOCATION 的循环没有调用 validCoord 归一化坐标") } } // 端到端:脏备份走一遍真实的 Parse,不许把伪时间/越界坐标带进领域模型。 func TestParseRejectsDirtyTimeAndCoords(t *testing.T) { good := time.Date(2026, 9, 20, 8, 0, 0, 0, time.UTC) goodSec := good.Sub(appleEpoch).Seconds() path := filepath.Join(t.TempDir(), "Backup.sqlite") sdb, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path)) if err != nil { t.Fatal(err) } defer sdb.Close() for _, stmt := range []string{ `CREATE TABLE ZTAGGROUP (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT)`, `CREATE TABLE ZTAG (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZGROUP INTEGER)`, `CREATE TABLE ZACTIVITY (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZICON_ TEXT, ZISHOME INTEGER, ZISWORK INTEGER, ZISEXCLUDED INTEGER, ZISARCHIVED INTEGER, ZUID_ BLOB)`, `CREATE TABLE ZTRANSPORT (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZICON_ TEXT)`, `CREATE TABLE ZLOCATION (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCATEGORY_ TEXT, ZLATITUDE REAL, ZLONGITUDE REAL, ZISOCOUNTRYCODE TEXT, ZADMINISTRATIVEAREA TEXT, ZLOCALITY TEXT, ZSUBLOCALITY TEXT, ZTHOROUGHFARE TEXT, ZSUBTHOROUGHFARE TEXT, ZTIMEZONE TEXT)`, `CREATE TABLE ZVISIT (Z_PK INTEGER PRIMARY KEY, ZLOCATION INTEGER, ZACTIVITY_ INTEGER, ZARRIVALDATE_ REAL, ZDEPARTUREDATE_ REAL, ZBOOKMARKED INTEGER, ZUSERADDED INTEGER, ZREMARK_ TEXT, ZEMOJINAME_ TEXT, ZWEATHERSYMBOL_ TEXT, ZIDENTIFIER_ BLOB)`, `CREATE TABLE Z_11VISITS_ (Z_18VISITS_ INTEGER, Z_11TAGS_5 INTEGER)`, `CREATE TABLE ZMOVEMENT (Z_PK INTEGER PRIMARY KEY, ZTRANSPORT_ INTEGER, ZVISITFROM_ INTEGER, ZVISITTO_ INTEGER, ZSTART_ REAL, ZEND_ REAL)`, `CREATE TABLE ZHOURLYWEATHER (Z_PK INTEGER PRIMARY KEY, ZVISIT INTEGER, ZDATE_ REAL, ZISDAYLIGHT INTEGER, ZTEMPERATURE_ REAL, ZAPPARENTTEMPERATURE_ REAL, ZHUMIDITY_ REAL, ZPRECIPITATIONAMOUNT_ REAL, ZPRECIPITATIONCHANCE_ REAL, ZWINDSPEED_ REAL, ZVISIBILITY_ REAL, ZUVINDEXVALUE_ INTEGER, ZCONDITION_ TEXT, ZSYMBOLNAME_ TEXT, ZUVINDEXCATEGORY_ TEXT, ZWINDCOMPASSDIRECTION_ TEXT)`, } { if _, err := sdb.Exec(stmt); err != nil { t.Fatalf("建表失败: %v", err) } } exec := func(q string, args ...any) { t.Helper() if _, err := sdb.Exec(q, args...); err != nil { t.Fatalf("插入失败: %v", err) } } exec(`INSERT INTO ZACTIVITY (Z_PK, ZNAME_, ZCOLOR_, ZICON_, ZISHOME, ZISWORK, ZISEXCLUDED, ZISARCHIVED) VALUES (1,'通勤','#fff','🚇',0,0,0,0)`) // 1=正常;2=坐标越界;3=坐标量级离谱(模拟 NaN 入库前的形态) exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (1,'正常',24.1,109.1)`) exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (2,'越界',999,-999)`) exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (3,'离谱',1e30,0)`) // 1=正常时间;2=ZARRIVALDATE_ 落到 2123 年;3=量级离谱(旧实现回绕成 2001) exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_, ZDEPARTUREDATE_) VALUES (1,1,1,?,?)`, goodSec, goodSec+3600) exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_) VALUES (2,2,1,1e12)`) exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_) VALUES (3,3,1,-1e30)`) // 位移:时间坏了保留「有行、无时间」,不能写成伪时间 exec(`INSERT INTO ZMOVEMENT (Z_PK, ZTRANSPORT_, ZVISITFROM_, ZVISITTO_, ZSTART_, ZEND_) VALUES (1,1,1,1,?,?)`, goodSec, goodSec+600) exec(`INSERT INTO ZMOVEMENT (Z_PK, ZTRANSPORT_, ZVISITFROM_, ZVISITTO_, ZSTART_, ZEND_) VALUES (2,1,1,1,1e12,1e12)`) b, err := Parse(sdb) if err != nil { t.Fatal(err) } byLoc := map[int]Location{} for _, l := range b.Locations { byLoc[l.SrcPK] = l } if got := byLoc[1]; got.Lat != 24.1 || got.Lon != 109.1 { t.Errorf("正常坐标被改动了: (%v,%v)", got.Lat, got.Lon) } if got := byLoc[2]; got.Lat != 0 || got.Lon != 0 { t.Errorf("越界坐标应归零,实际 (%v,%v)", got.Lat, got.Lon) } if got := byLoc[3]; got.Lat != 0 || got.Lon != 0 { t.Errorf("量级离谱的坐标应归零,实际 (%v,%v)", got.Lat, got.Lon) } byVisit := map[int]Visit{} for _, v := range b.Visits { byVisit[v.SrcPK] = v } if v := byVisit[1]; v.Arrival == nil || !v.Arrival.Equal(good) { t.Errorf("正常到访时间丢了: %v", v.Arrival) } for _, pk := range []int{2, 3} { if v := byVisit[pk]; v.Arrival != nil { t.Errorf("到访 #%d 的脏时间应当被拒,实际 %s", pk, v.Arrival.Format(time.RFC3339)) } } if len(b.Movements) != 2 { t.Fatalf("位移条数不对: %d", len(b.Movements)) } if m := b.Movements[0]; m.Start == nil || !m.Start.Equal(good) { t.Errorf("正常位移时间丢了: %v", m.Start) } if m := b.Movements[1]; m.Start != nil || m.End != nil { t.Errorf("脏位移时间应当为空而不是伪时间: start=%v end=%v", m.Start, m.End) } }