From 9aab8f1b31e188a6b9e89a7a4cb06cf9dbc63895 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Fri, 30 Oct 2026 21:59:51 -0300 Subject: [PATCH] mutate: a run's TMPDIR is made under the caller's, not inside the tree Each run's TMPDIR was /trees//tmp. A test's t.TempDir() adds its name and two levels more, so a test with a long name that binds a unix socket there went past the 108 bytes a socket path may have: it failed with nothing broken, and the lines only it reached came out SURVIVED. Eight tests in spin hit this, among them cmd/supervisor's keep tests on the fleet gate. Co-Authored-By: Claude Opus 5.5 --- internal/mutate/tree.go | 7 +++-- internal/mutate/tree_internal_test.go | 38 +++++++++++++++++++++++++++ site/src/content/docs/mutate.mdx | 4 ++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/mutate/tree.go b/internal/mutate/tree.go index ac63500..c415212 100644 --- a/internal/mutate/tree.go +++ b/internal/mutate/tree.go @@ -80,8 +80,11 @@ func (t *Tree) in(dir string, run func(at, tmp string)) error { if err != nil { return err } - tmp := filepath.Join(t.dir, "tmp") - if err := os.MkdirAll(tmp, 0o700); err != nil { + // Not under the tree's own directory: a test's t.TempDir() is its name and two levels more + // under this, and a socket there, from a test with a long name, was past the 108 bytes a unix + // socket's path may have - so the test failed with nothing broken, and its lines went unasked. + tmp, err := os.MkdirTemp("", "m") + if err != nil { return err } run(filepath.Join(c.dst, rel), tmp) diff --git a/internal/mutate/tree_internal_test.go b/internal/mutate/tree_internal_test.go index 928b9fb..3ba26b3 100644 --- a/internal/mutate/tree_internal_test.go +++ b/internal/mutate/tree_internal_test.go @@ -2,6 +2,7 @@ package mutate import ( "io/fs" + "net" "os" "os/exec" "path/filepath" @@ -62,6 +63,43 @@ func TestATreeIsTheModuleAsGitSeesItPutBackAfterEachRun(t *testing.T) { } } +// A run's temporary directory is empty, its own and gone once it returns, and short enough that +// a test with a long name binds a socket in its t.TempDir() there, however deep the tree is. +func TestARunsTemporaryDirectoryTakesASocketFromATestWithALongName(t *testing.T) { + root := gitModule(t) + tree := NewTree(filepath.Join(t.TempDir(), "trees", "12", "a-tree-as-deep-as-a-runner-makes-it")) + var tmps []string + for range 2 { + err := tree.in(filepath.Join(root, "p"), func(_, tmp string) { + tmps = append(tmps, tmp) + if entries, err := os.ReadDir(tmp); err != nil || len(entries) != 0 { + t.Errorf("the run's temporary directory holds %v (%v), not nothing", entries, err) + } + // As t.TempDir() makes one: the test's name and a number, then a directory per call. + dir := filepath.Join(tmp, "TestTheKeepCommandHoldsALeaseForAsLongAsItsCommandRuns1008086323", "001") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + l, err := net.Listen("unix", filepath.Join(dir, "identity.sock")) + if err != nil { + t.Fatalf("a socket in a long-named test's directory: %v", err) + } + _ = l.Close() // only that it bound is asked + }) + if err != nil { + t.Fatal(err) + } + } + if tmps[0] == tmps[1] { + t.Errorf("two runs shared the temporary directory %s", tmps[0]) + } + for _, tmp := range tmps { + if _, err := os.Stat(tmp); err == nil { + t.Errorf("%s outlived its run", tmp) + } + } +} + // gitModule is a module in a git work tree: files tracked, one neither tracked nor ignored, an // ignored file in its package p, and a directory it ignores. func gitModule(t *testing.T) string { diff --git a/site/src/content/docs/mutate.mdx b/site/src/content/docs/mutate.mdx index b2ce8e5..c91de6b 100644 --- a/site/src/content/docs/mutate.mdx +++ b/site/src/content/docs/mutate.mdx @@ -199,7 +199,9 @@ edits ran side by side. Instead, every test process runs in a private copy of th also gets an empty temporary directory of its own as `TMPDIR`, removed after the run. Without it, two runs of the same tests could share a path under `os.TempDir()`, such as a cache folder named by the current time. One of them would fail with nothing broken, and the edits only that test -reaches would be let through. +reaches would be let through. The directory is made directly under your own `TMPDIR`, not inside +the copy: a test with a long name that binds a unix socket in its `t.TempDir()` stays under the +108 bytes such a path may have. The copy holds what git sees: tracked files and files git doesn't ignore. Directories git ignores (`node_modules`, a build's output) and repositories nested in the module are linked rather than