Artifact ccefde75878eebf8655d0172b7d9a0390e373a6e349b1ed569309d4c59e83cb6:
- File
testutil/testutil.go
— part of check-in
[f804878bd3]
at
2026-07-22 23:58:46
on branch trunk
— test: route every fossil-binary gate through one REQUIRE_FOSSIL_BIN helper (#125)
* test: route fossil-binary tests through one REQUIRE_FOSSIL_BIN helper
REQUIRE_FOSSIL_BIN=1 turns a missing fossil binary into a test failure instead of an invisible skip, but only internal/manifest honored it. The six other fossil-dependent sites each did their own exec.LookPath + unconditional t.Skip, so on those a run with no fossil was byte-identical to a passing one.
Add testutil.RequireFossilBin, which resolves the binary the same way FossilBinary does (FOSSIL_BIN override, then PATH) and either skips or fails based on REQUIRE_FOSSIL_BIN. Route testutil.NewTestRepo, both internal/delta CLI round-trips, the four internal/merge validation tests, the internal/xfer sanity check, and the ls-branch-tip repro through it, deleting their bespoke lookup+skip logic. CI already sets REQUIRE_FOSSIL_BIN=1 on the unit-test steps, so this makes all seven sites fail loud on absence while local runs without fossil stay opt-in.
* test: route every fossil-binary gate through RequireFossilBin
The first pass left six sites still deciding for themselves whether a missing fossil binary is a skip or a failure. Three of them (internal/sync's startFossilServer and its two integration tests, manifest's two rebuild tests, the root TestPhaseB_Integration, and hash's TestSHA1_FossilValidation) skipped unconditionally, so with REQUIRE_FOSSIL_BIN=1 and no fossil on PATH those packages still reported ok -- the exact silent pass the flag exists to prevent. The other three had hand-rolled copies of the flag check, which is the duplication that let the original drift happen.
All of them now call testutil.RequireFossilBin, and HasFossil is gone: it was a second, skip-only way to ask the same question, and it is what produced the sites that ignored the flag.
Also validate the FOSSIL_BIN override with LookPath, so a stale path reports "fossil not found" instead of surfacing later as a raw fork/exec error.
* test: resolve fossil binary via RequireFossilBin at remaining call sites
* fix: correct stale module path missed by rebase auto-merge
Three imports added by earlier commits in this branch predated the rebase target and had no corresponding line on main to conflict against, so git's auto-merge carried the old module path through verbatim. (user: [email protected], size: 3717) [annotate] [blame] [check-ins using]
package testutil import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) type TestRepo struct { Path string Dir string } // FossilBinary resolves the canonical fossil binary: the FOSSIL_BIN override // first, then PATH. An override that does not resolve to an executable is // treated as no binary at all, so the caller reports "fossil not found" rather // than letting a stale path surface later as a raw fork/exec error. func FossilBinary() string { if bin := os.Getenv("FOSSIL_BIN"); bin != "" { path, err := exec.LookPath(bin) if err != nil { return "" } return path } path, err := exec.LookPath("fossil") if err != nil { return "" } return path } // RequireFossilBin resolves the fossil binary for tests that need canonical // Fossil to run. It resolves the same way FossilBinary does (the FOSSIL_BIN // override, then PATH). When no binary is found it skips the test, unless // REQUIRE_FOSSIL_BIN=1 is set -- then it fails. That makes CI turn a missing // binary into a loud failure (verifying real Fossil reads what we write is the // one thing our own tests cannot substitute for) while local runs without // fossil installed stay an opt-in skip. func RequireFossilBin(t *testing.T) string { t.Helper() if bin := FossilBinary(); bin != "" { return bin } where := "set FOSSIL_BIN or install fossil on PATH" if bin := os.Getenv("FOSSIL_BIN"); bin != "" { where = fmt.Sprintf("FOSSIL_BIN=%q is not an executable", bin) } if os.Getenv("REQUIRE_FOSSIL_BIN") == "1" { t.Fatalf("REQUIRE_FOSSIL_BIN=1 but no fossil binary found (%s)", where) } t.Skipf("fossil binary not found (%s); set REQUIRE_FOSSIL_BIN=1 to require it", where) return "" } func NewTestRepo(t *testing.T) *TestRepo { t.Helper() bin := RequireFossilBin(t) dir := t.TempDir() path := filepath.Join(dir, "test.fossil") cmd := exec.Command(bin, "new", path) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil new failed: %v\n%s", err, out) } return &TestRepo{Path: path, Dir: dir} } func NewTestRepoFromPath(t *testing.T, path string) *TestRepo { t.Helper() abs, err := filepath.Abs(path) if err != nil { t.Fatalf("cannot resolve path %q: %v", path, err) } return &TestRepo{Path: abs, Dir: filepath.Dir(abs)} } func (r *TestRepo) FossilRebuild(t *testing.T) { t.Helper() cmd := exec.Command(FossilBinary(), "rebuild", r.Path) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("fossil rebuild failed: %v\n%s", err, out) } } func (r *TestRepo) FossilArtifact(t *testing.T, uuid string) []byte { t.Helper() cmd := exec.Command(FossilBinary(), "artifact", uuid, "-R", r.Path) out, err := cmd.Output() if err != nil { t.Fatalf("fossil artifact %s failed: %v", uuid, err) } return out } func FossilRebuild(repoPath string) error { cmd := exec.Command(FossilBinary(), "rebuild", repoPath) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("fossil rebuild: %v\n%s", err, out) } return nil } func FossilTimeline(repoPath string) ([]byte, error) { cmd := exec.Command(FossilBinary(), "timeline", "-n", "100", "-R", repoPath) return cmd.Output() } func FossilArtifactByPath(repoPath, uuid string) ([]byte, error) { cmd := exec.Command(FossilBinary(), "artifact", uuid, "-R", repoPath) return cmd.Output() } func (r *TestRepo) FossilSQL(t *testing.T, sql string) string { t.Helper() cmd := exec.Command(FossilBinary(), "sql", "-R", r.Path, sql) out, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { t.Fatalf("fossil sql failed: %v\nstderr: %s", err, exitErr.Stderr) } t.Fatalf("fossil sql failed: %v", err) } return strings.TrimSpace(string(out)) }