Artifact 8b0db7aca095f627393f616cbc240b94965c092ee405d7afeeeb7be78b56406a:
- File
testutil/isolate_test.go
— part of check-in
[8d3f18265e]
at
2026-10-13 01:10:27
on branch main
— fix(testutil): isolate fossil's global config in tests (#284) (#366)
* test: isolate fossil's global config in tests that run the fossil binary (#284)
Tracked in #284.
* test(testutil): document the file-level scope of the fossil home guard (#284)
* test(testutil): allow StartServer in the fossil home guard (#284) (user: [email protected], size: 4118) [annotate] [blame] [check-ins using]
package testutil import ( "io/fs" "os" "os/exec" "path/filepath" "strings" "testing" ) // fossilConfigPaths lists the per-user databases fossil may create under home. func fossilConfigPaths(home string) []string { return []string{ filepath.Join(home, ".fossil"), filepath.Join(home, ".config", "fossil.db"), } } // TestIsolateFossilHomeKeepsGlobalConfigOut proves a fossil run after // RequireFossilBin writes its global config under the per-test directory (#284). // It does not compare the real files' mtimes: other processes on a shared // machine legitimately touch them, which would make the test flaky. func TestIsolateFossilHomeKeepsGlobalConfigOut(t *testing.T) { realHome, err := os.UserHomeDir() if err != nil { t.Skipf("no real home to compare against: %v", err) } bin := RequireFossilBin(t) home := os.Getenv("FOSSIL_HOME") if home == "" || home == realHome || os.Getenv("HOME") != home { t.Fatalf("HOME=%q FOSSIL_HOME=%q: want both set to one per-test dir, not %q", os.Getenv("HOME"), home, realHome) } if !strings.HasPrefix(os.Getenv("XDG_CONFIG_HOME"), home) { t.Fatalf("XDG_CONFIG_HOME=%q is outside the isolated home %q", os.Getenv("XDG_CONFIG_HOME"), home) } // `fossil new` records the repository in the global config. repoPath := filepath.Join(t.TempDir(), "iso.fossil") if out, err := exec.Command(bin, "new", repoPath).CombinedOutput(); err != nil { t.Fatalf("fossil new: %v\n%s", err, out) } found := false for _, p := range fossilConfigPaths(home) { if _, err := os.Stat(p); err == nil { found = true } } if !found { t.Fatalf("fossil wrote no global config under %q; it is using another home", home) } } // fossilIsolationMarkers are the substrings that show a test file resolves // fossil through the isolating helpers or sets its own private home. var fossilIsolationMarkers = []string{ "requirefossilbin(", "isolatefossilhome(", "newtestrepo(", "fossil_home=", } // notFossilExec lists test files whose exec.Command calls never run fossil. var notFossilExec = map[string]string{ "cmd/libfossil/main_test.go": "re-executes the test binary", "cmd/libfossil/driver_dependencies_test.go": "runs the go tool", "testutil/server_unix.go": "runs the bin its caller resolved", "testutil/server_other.go": "runs the bin its caller resolved", } // TestFossilInvocationsIsolateHome fails when a Go file runs a subprocess // without any means of isolating fossil's home. It is a static net for new // tests: a test that shells out to fossil must resolve it through // RequireFossilBin (or call IsolateFossilHome), so the developer's real // ~/.fossil and ~/.config/fossil.db are never written (#284). // // The check is per file, not per function: one isolating marker anywhere in a // file clears every exec.Command in it, so a file whose helpers call fossil // directly still relies on its tests calling RequireFossilBin first. The // markers match whole call names ("newtestrepo(" does not match // NewTestRepoFromPath), so a helper that does not isolate does not count. func TestFossilInvocationsIsolateHome(t *testing.T) { root := ".." err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if n := d.Name(); n == ".git" || n == "node_modules" || n == "docs" { return fs.SkipDir } return nil } if !strings.HasSuffix(path, ".go") { return nil } src, rerr := os.ReadFile(path) if rerr != nil { return rerr } text := string(src) if !strings.Contains(text, "exec.Command") { return nil } rel := filepath.ToSlash(strings.TrimPrefix(path, root+string(filepath.Separator))) if _, ok := notFossilExec[rel]; ok { return nil } lower := strings.ToLower(text) for _, m := range fossilIsolationMarkers { if strings.Contains(lower, m) { return nil } } t.Errorf("%s runs exec.Command without testutil.RequireFossilBin or "+ "IsolateFossilHome; fossil would write the real global config", rel) return nil }) if err != nil { t.Fatal(err) } }