Artifact b9697b25a517f6b7204d36eb4574262256053d47d449ef482e4c210333e88b7f:
- File
internal/undo/fossilbin_test.go
— part of check-in
[41951ffbc5]
at
2026-10-19 01:53:57
on branch feat/submanifest-reader
— fix(stash,undo): write working files through the checkout's storage
Stash and undo read, wrote and removed working files with the os package, so a MemStorage checkout and simulation tests never saw those writes, and no write recorded its mtime. They now take a *simio.Env and go through its Storage, recording each written file's mtime via vfile.WrittenMTime (moved with RecordableMTime from internal/checkout so these packages can share the same-second guard).
Stash save marks reverted rows unchanged with their written mtime, as fossil's revert does. Apply and pop add a stashed added file again, as fossil's stash_apply does, and give a restored edit's row its changed state and written mtime. Undo and redo record a written file's mtime only when the restored row's mtime matched the file when it was saved.
Tracked in #448 (user: [email protected], size: 3682) [annotate] [blame] [check-ins using] [more...]
//go:build !js package undo_test import ( "database/sql" "os" "os/exec" "path/filepath" "strings" "testing" "time" libdb "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/undo" "github.com/danmestas/go-libfossil/simio" "github.com/danmestas/go-libfossil/testutil" ) // recordedMTime fails unless name's vfile.mtime is its file's mtime. func recordedMTime(t *testing.T, ckout *sql.DB, ck, name string) { t.Helper() var mtime int64 if err := ckout.QueryRow( "SELECT CAST(coalesce(mtime, 0) AS INTEGER) FROM vfile WHERE pathname=?", name, ).Scan(&mtime); err != nil { t.Fatal(err) } info, err := os.Stat(filepath.Join(ck, name)) if err != nil { t.Fatal(err) } if mtime != info.ModTime().Unix() { t.Fatalf("%s: vfile.mtime %d, file mtime %d", name, mtime, info.ModTime().Unix()) } } func fossilIn(t *testing.T, bin, dir string, args ...string) string { t.Helper() cmd := exec.Command(bin, args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil %s: %v\n%s", strings.Join(args, " "), err, out) } return strings.TrimSpace(string(out)) } // withCheckoutDB opens the checkout's database for fn and closes it before // fossil runs again. func withCheckoutDB(t *testing.T, ck string, fn func(ckout *sql.DB)) { t.Helper() // OpenSQL turns foreign keys off, as fossil runs and as checkouts open // this database: fossil's vfile references a blob table it does not have. ckout, err := libdb.OpenSQL(filepath.Join(ck, ".fslckout"), libdb.OpenConfig{}, nil) if err != nil { t.Fatal(err) } defer ckout.Close() fn(ckout) } // TestUndoVFileReadByFossil: on a checkout fossil scanned, undo here // restores the file and the row fossil recorded for it, now with the written // file's mtime, and redo the edit; fossil's own scan then reports what is on // disk each time. func TestUndoVFileReadByFossil(t *testing.T) { bin := testutil.RequireFossilBin(t) // A clock an hour ahead makes each file written here recordable at once. env := &simio.Env{ Clock: simio.NewSimClockAt(time.Now().Add(time.Hour)), Rand: simio.CryptoRand{}, Storage: simio.OSStorage{}, } dir := t.TempDir() repoPath := filepath.Join(dir, "r.fossil") ck := filepath.Join(dir, "ck") if err := os.Mkdir(ck, 0o755); err != nil { t.Fatal(err) } fossilIn(t, bin, dir, "init", repoPath) fossilIn(t, bin, ck, "open", repoPath) a := filepath.Join(ck, "a.txt") if err := os.WriteFile(a, []byte("hello world\n"), 0o644); err != nil { t.Fatal(err) } fossilIn(t, bin, ck, "add", "a.txt") fossilIn(t, bin, ck, "commit", "-m", "v1") if got := fossilIn(t, bin, ck, "changes"); got != "" { t.Fatalf("fossil changes after commit = %q", got) } withCheckoutDB(t, ck, func(ckout *sql.DB) { if err := undo.Save(ckout, env, ck, nil); err != nil { t.Fatal(err) } }) if err := os.WriteFile(a, []byte("hello world, edited\n"), 0o644); err != nil { t.Fatal(err) } if got := fossilIn(t, bin, ck, "changes"); got != "EDITED a.txt" { t.Fatalf("fossil changes after edit = %q", got) } withCheckoutDB(t, ck, func(ckout *sql.DB) { if err := undo.Undo(ckout, env, ck); err != nil { t.Fatal(err) } recordedMTime(t, ckout, ck, "a.txt") }) if got := fossilIn(t, bin, ck, "changes"); got != "" { t.Fatalf("fossil changes after undo = %q", got) } withCheckoutDB(t, ck, func(ckout *sql.DB) { if err := undo.Redo(ckout, env, ck); err != nil { t.Fatal(err) } recordedMTime(t, ckout, ck, "a.txt") }) if got := fossilIn(t, bin, ck, "changes"); got != "EDITED a.txt" { t.Fatalf("fossil changes after redo = %q", got) } fossilIn(t, bin, ck, "close", "--force") }