detached 650dc0a

test(githttp): write the fake backend once to avoid ETXTBSY under parallel tests

Writing an executable while parallel tests fork lets a child inherit the
write descriptor, and exec of the script then fails with "text file busy"
(golang/go#22315). The fake git-http-backend is written once in TestMain, and
the hook script test that writes executables runs serially.
commit
650dc0ac5f1156f67297bfa794dcf5a7f61a1c6d
author
brice · · 2026-10-12 06:55 UTC
parent
588fe2c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +28 -4
M internal/adapters/githttp/handler_test.go +25 -3
@@ -31,12 +31,34 @@ import (
3131 // environment and the body it received. Like Git, it reads the whole request
3232 // before it answers (net/http cannot read a request body once the response
3333 // has started).
34+//
35+// The script is written once, in TestMain, before any test runs: writing an
36+// executable while parallel tests fork lets a child inherit the open write
37+// descriptor, and exec then fails with ETXTBSY (golang/go#22315).
3438 func cgiScript(t *testing.T) string {
3539 t.Helper()
36- p := filepath.Join(t.TempDir(), "backend")
40+ require.NotEmpty(t, backendScript, "TestMain writes the backend script")
41+ return backendScript
42+}
43+
44+var backendScript string
45+
46+func TestMain(m *testing.M) {
47+ os.Exit(runWithBackendScript(m))
48+}
49+
50+func runWithBackendScript(m *testing.M) int {
51+ dir, err := os.MkdirTemp("", "githttp-backend")
52+ if err != nil {
53+ panic(err)
54+ }
55+ defer func() { _ = os.RemoveAll(dir) }()
56+ backendScript = filepath.Join(dir, "backend")
3757 body := "#!/bin/sh\nin=$(mktemp)\ncat > \"$in\"\nprintf 'Content-Type: text/plain\\r\\n\\r\\n'\nenv | sort\nprintf '\\n--body--\\n'\ncat \"$in\"\nrm -f \"$in\"\n"
38- require.NoError(t, os.WriteFile(p, []byte(body), 0o755))
39- return p
58+ if err := os.WriteFile(backendScript, []byte(body), 0o755); err != nil {
59+ panic(err)
60+ }
61+ return m.Run()
4062 }
4163
4264 type rig struct {
@@ -31,12 +31,34 @@ import (
31 // environment and the body it received. Like Git, it reads the whole request31 // environment and the body it received. Like Git, it reads the whole request
32 // before it answers (net/http cannot read a request body once the response32 // before it answers (net/http cannot read a request body once the response
33 // has started).33 // has started).
34+//
35+// The script is written once, in TestMain, before any test runs: writing an
36+// executable while parallel tests fork lets a child inherit the open write
37+// descriptor, and exec then fails with ETXTBSY (golang/go#22315).
34 func cgiScript(t *testing.T) string {38 func cgiScript(t *testing.T) string {
35 t.Helper()39 t.Helper()
36- p := filepath.Join(t.TempDir(), "backend")40+ require.NotEmpty(t, backendScript, "TestMain writes the backend script")
41+ return backendScript
42+}
43+
44+var backendScript string
45+
46+func TestMain(m *testing.M) {
47+ os.Exit(runWithBackendScript(m))
48+}
49+
50+func runWithBackendScript(m *testing.M) int {
51+ dir, err := os.MkdirTemp("", "githttp-backend")
52+ if err != nil {
53+ panic(err)
54+ }
55+ defer func() { _ = os.RemoveAll(dir) }()
56+ backendScript = filepath.Join(dir, "backend")
37 body := "#!/bin/sh\nin=$(mktemp)\ncat > \"$in\"\nprintf 'Content-Type: text/plain\\r\\n\\r\\n'\nenv | sort\nprintf '\\n--body--\\n'\ncat \"$in\"\nrm -f \"$in\"\n"57 body := "#!/bin/sh\nin=$(mktemp)\ncat > \"$in\"\nprintf 'Content-Type: text/plain\\r\\n\\r\\n'\nenv | sort\nprintf '\\n--body--\\n'\ncat \"$in\"\nrm -f \"$in\"\n"
38- require.NoError(t, os.WriteFile(p, []byte(body), 0o755))58+ if err := os.WriteFile(backendScript, []byte(body), 0o755); err != nil {
39- return p59+ panic(err)
60+ }
61+ return m.Run()
40 }62 }
41 63
42 type rig struct {64 type rig struct {
M internal/adapters/githttp/hooks_test.go +3 -1
@@ -390,8 +390,10 @@ func TestWriteHookScripts(t *testing.T) {
390390 }
391391
392392 // The shell really passes the awkward path and the arguments through.
393+//
394+// Not parallel: it writes executables, and a test that forks meanwhile would
395+// inherit the write descriptor and make exec fail with ETXTBSY.
393396 func TestHookScriptExecsTheBinaryWithQuotedPath(t *testing.T) {
394- t.Parallel()
395397 base := filepath.Join(t.TempDir(), "it's a \"dir\" $X `y` ; z")
396398 require.NoError(t, os.MkdirAll(base, 0o755))
397399 exe := filepath.Join(base, "fake ermite")
@@ -390,8 +390,10 @@ func TestWriteHookScripts(t *testing.T) {
390 }390 }
391 391
392 // The shell really passes the awkward path and the arguments through.392 // The shell really passes the awkward path and the arguments through.
393+//
394+// Not parallel: it writes executables, and a test that forks meanwhile would
395+// inherit the write descriptor and make exec fail with ETXTBSY.
393 func TestHookScriptExecsTheBinaryWithQuotedPath(t *testing.T) {396 func TestHookScriptExecsTheBinaryWithQuotedPath(t *testing.T) {
394- t.Parallel()
395 base := filepath.Join(t.TempDir(), "it's a \"dir\" $X `y` ; z")397 base := filepath.Join(t.TempDir(), "it's a \"dir\" $X `y` ; z")
396 require.NoError(t, os.MkdirAll(base, 0o755))398 require.NoError(t, os.MkdirAll(base, 0o755))
397 exe := filepath.Join(base, "fake ermite")399 exe := filepath.Join(base, "fake ermite")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 43

↑↓ moveenter openesc close