Artifact 980725f5c7a36db93546639d3c7769588ca8c73e0e1ae64197151b4411ef503d:
- File
clone_realfossil_content_type_test.go
— part of check-in
[2256f5d7ce]
at
2026-07-29 19:19:43
on branch trunk
— fix(test): reap fossil servers before temp-dir cleanup; raise unit-test timeout (#220)
Three tests started a real fossil server and killed it with Process.Kill() without waiting for the exit. Kill signals but does not reap, so the server could still be writing its -wal/-shm files while t.TempDir()'s cleanup walked the directory. A file appearing mid-walk fails RemoveAll with "directory not empty", which is how TestCloneFromRealFossilUncompressedReply failed on CI while passing locally -- it needs a loaded machine to lose the race.
internal/sync/integration_test.go already did this correctly, Kill followed by Wait, with a comment naming the same failure. This applies that established pattern at the three sites that lacked it.
Separately, the unit-test step ran with -timeout=120s, a per-package budget. CI runs the package binaries concurrently, so under contention internal/manifest and internal/sync exceeded it and panicked with "test timed out" despite finishing in 19s and 34s on an unloaded runner. Raised to 300s, matching the DST step, so the margin does not depend on how busy the runner is. (user: [email protected], size: 4157) [annotate] [blame] [check-ins using]
package libfossil_test import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "testing" "time" libfossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/testutil" ) // TestCloneFromRealFossilUncompressedReply is the end-to-end regression for // issue #164: cloning through the public HTTP Transport against a real fossil // server must succeed on the very first exchange round, even though that // server's clone reply uses the §4 uncompressed/plain-card framing rather than // the §4.1 compressed container. // // Before the fix, the adapter bridging the public byte-oriented Transport to // the internal xfer decoder hardcoded the compressed framing. Against a real // fossil server it read the first four bytes of the plain card text // (`prag`... from `pragma server-version ...`) as a big-endian length prefix, // so decode failed immediately with "declared container length ... exceeds ... // bytes" — round one, before a single card was parsed. // // This drives a genuine clone against a real fossil-served repository and is // skipped cleanly when no fossil binary is present. func TestCloneFromRealFossilUncompressedReply(t *testing.T) { bin := testutil.RequireFossilBin(t) dir := t.TempDir() srcRepo := filepath.Join(dir, "source.fossil") // Build a small real fossil repository with content to clone. runFossil(t, bin, dir, "init", srcRepo) work := filepath.Join(dir, "work") if err := os.MkdirAll(work, 0o755); err != nil { t.Fatalf("mkdir work: %v", err) } runFossil(t, bin, work, "open", srcRepo) if err := os.WriteFile(filepath.Join(work, "hello.txt"), []byte("hello from real fossil\n"), 0o644); err != nil { t.Fatalf("write hello.txt: %v", err) } runFossil(t, bin, work, "add", "hello.txt") runFossil(t, bin, work, "commit", "-m", "initial commit", "--no-warnings") // Serve it over HTTP. --localhost auto-authenticates localhost requests as // the setup user, so an anonymous clone has read access. port := freeTCPPort(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() server := exec.CommandContext(ctx, bin, "server", srcRepo, "--localhost", "--port", fmt.Sprintf("%d", port)) server.Stdout = os.Stderr server.Stderr = os.Stderr if err := server.Start(); err != nil { t.Fatalf("start fossil server: %v", err) } // Kill signals but does not reap; wait for the exit so the server is not // still writing into the temp dir when cleanup removes it. defer func() { _ = server.Process.Kill() _ = server.Wait() }() addr := fmt.Sprintf("127.0.0.1:%d", port) waitTCP(t, addr) // Clone through the public HTTP transport — the path that carried the bug. url := "http://" + addr + "/" clonePath := filepath.Join(dir, "clone.fossil") repo, result, err := libfossil.Clone(ctx, clonePath, libfossil.NewHTTPTransport(url), libfossil.CloneOpts{}) if err != nil { t.Fatalf("clone from real fossil server failed: %v", err) } defer repo.Close() if result.Rounds < 1 { t.Fatalf("clone reported %d rounds, want >= 1", result.Rounds) } if result.BlobsRecvd < 1 { t.Fatalf("clone received %d blobs, want >= 1 (the committed artifacts)", result.BlobsRecvd) } } // runFossil runs a fossil subcommand in dir and fails the test on error. func runFossil(t *testing.T, bin, dir string, args ...string) { t.Helper() cmd := exec.Command(bin, args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("fossil %v: %v\n%s", args, err, out) } } // freeTCPPort reserves an ephemeral TCP port and returns it. func freeTCPPort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("reserve port: %v", err) } defer l.Close() return l.Addr().(*net.TCPAddr).Port } // waitTCP dials addr until it accepts or the deadline passes. func waitTCP(t *testing.T, addr string) { t.Helper() deadline := time.Now().Add(10 * time.Second) for time.Now().Before(deadline) { c, err := net.DialTimeout("tcp", addr, 200*time.Millisecond) if err == nil { c.Close() return } time.Sleep(20 * time.Millisecond) } t.Fatalf("port %s never accepted", addr) }