Artifact b96a8e54ba8d0d9c642895db54389efe114aa72ea3960abd7acad8634b313a5a:
- File
internal/fsltype/julian_sweep_test.go
— part of check-in
[387204fa55]
at
2026-10-19 02:58:03
on branch feat/submanifest-reader
— fix(fsltype): compute text Julian days in the platform's long double
fossil's printf carries a %.17g value in C long double: a 64-bit double on macOS arm64 and Windows, 80-bit x87 on x86-64 Linux. The 16th digit, and so the event time fossil stores, differs between the two for about one time in five, which is why TestTextJulianDayMatchesFossilBinary passed on macOS and failed against CI's linux-x64 fossil. SQLite is not involved: julianday() counts integer milliseconds and 3.52.0's text-to-double conversion is integer arithmetic, neither using LONGDOUBLE_TYPE.
fossilPrintf17g now runs fossil's algorithm in a big.Float with the significand width of the platform's long double (53, 64 or 113 bits). Against all 4,007 times fossil stored in the failing CI run, 849 of them different from macOS fossil's, the 64-bit form matches every one; the 53-bit form still matches macOS fossil. The binary128 width (arm64 Linux) follows the same arithmetic but has not been compared with such a fossil. Times written on one platform and rebuilt by fossil on another may differ in the last place, as two fossils do. (user: [email protected], size: 3901) [annotate] [blame] [check-ins using] [more...]
package fsltype_test import ( "crypto/md5" "fmt" "math/rand" "os/exec" "path/filepath" "testing" "time" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/testutil" ) // sweepMillis are card times to the millisecond: random ones from 1970 to // 2100, every millisecond of one second, and the times of the fossil // repository's rows that told the conversions apart. func sweepMillis(n int) []int64 { rng := rand.New(rand.NewSource(13411)) out := []int64{1302747281361, 1313399551889, 1456907938278, 1499253127655, 1500607170788, 1597715665130, 1703782442848} for i := 0; i < n; i++ { out = append(out, rng.Int63n(4102444800000)) } base := time.Date(2011, 8, 15, 9, 12, 31, 0, time.UTC).UnixMilli() for ms := int64(0); ms < 1000; ms++ { out = append(out, base+ms) } return out } func cardText(millis int64) string { return time.UnixMilli(millis).UTC().Format("2006-01-02T15:04:05.000") } // TestJulianDayMatchesSQLiteJulianday checks JulianDayFromMillis against // julianday() of the card text in the SQLite this build links, bit for bit. func TestJulianDayMatchesSQLiteJulianday(t *testing.T) { d, err := db.OpenSQL(filepath.Join(t.TempDir(), "j.db"), db.OpenConfig{}, nil) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) for _, ms := range sweepMillis(20000) { var want float64 if err := d.QueryRow("SELECT julianday(?)", cardText(ms)).Scan(&want); err != nil { t.Fatal(err) } if got := fsltype.JulianDayFromMillis(ms); got != want { t.Fatalf("%s: JulianDayFromMillis = %.17g, julianday() = %.17g", cardText(ms), got, want) } } } // TestTextJulianDayMatchesFossilBinary stores a control artifact for each // sweep time in a repository fossil made, has fossil rebuild derive their // event rows -- each time written as SQL text by fossil's printf and read // back by its SQLite -- and requires TimeToTextJulian's value for every one. // fossil's printf works in long double, so this holds for a fossil binary // built for the platform the test runs on, as CI's and a developer's are. func TestTextJulianDayMatchesFossilBinary(t *testing.T) { bin := testutil.RequireFossilBin(t) path := filepath.Join(t.TempDir(), "t.fossil") if out, err := exec.Command(bin, "init", path).CombinedOutput(); err != nil { t.Fatalf("fossil init: %v\n%s", err, out) } r, err := repo.Open(path) if err != nil { t.Fatal(err) } var target string if err := r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=1").Scan(&target); err != nil { t.Fatal(err) } want := map[int64]float64{} for i, ms := range sweepMillis(3000) { body := fmt.Sprintf("D %s\nT +sym-t%d %s\nU u\n", cardText(ms), i, target) art := fmt.Sprintf("%sZ %x\n", body, md5.Sum([]byte(body))) rid, _, err := blob.Store(r.DB(), []byte(art)) if err != nil { t.Fatal(err) } want[int64(rid)] = fsltype.TimeToTextJulian(time.UnixMilli(ms)) } if err := r.Close(); err != nil { t.Fatal(err) } if out, err := exec.Command(bin, "rebuild", path).CombinedOutput(); err != nil { t.Fatalf("fossil rebuild: %v\n%s", err, out) } d, err := db.OpenSQL(path, db.OpenConfig{}, nil) if err != nil { t.Fatal(err) } defer d.Close() rows, err := d.Query("SELECT objid, CAST(mtime AS REAL) FROM event WHERE type='g'") if err != nil { t.Fatal(err) } defer rows.Close() n := 0 for rows.Next() { var rid int64 var got float64 if err := rows.Scan(&rid, &got); err != nil { t.Fatal(err) } n++ if got != want[rid] { t.Errorf("rid %d: fossil stored %.17g, TimeToTextJulian = %.17g", rid, got, want[rid]) } } if err := rows.Err(); err != nil { t.Fatal(err) } if n != len(want) { t.Fatalf("fossil derived %d tag events for %d control artifacts", n, len(want)) } }