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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 31 | 31 | // environment and the body it received. Like Git, it reads the whole request | |
| 32 | 32 | // before it answers (net/http cannot read a request body once the response | |
| 33 | 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 | 38 | func cgiScript(t *testing.T) string { | |
| 35 | 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 | 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)) | |
| 39 | - | return p | |
| 58 | + | if err := os.WriteFile(backendScript, []byte(body), 0o755); err != nil { | |
| 59 | + | panic(err) | |
| 60 | + | } | |
| 61 | + | return m.Run() | |
| 40 | 62 | } | |
| 41 | 63 | ||
| 42 | 64 | type rig struct { | |
| @@ -31,12 +31,34 @@ import ( | |||||
| 31 | // environment and the body it received. Like Git, it reads the whole request | 31 | // 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 response | 32 | // 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 p | 59 | + | 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) { | |||
| 390 | 390 | } | |
| 391 | 391 | ||
| 392 | 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 | 396 | func TestHookScriptExecsTheBinaryWithQuotedPath(t *testing.T) { | |
| 394 | - | t.Parallel() | |
| 395 | 397 | base := filepath.Join(t.TempDir(), "it's a \"dir\" $X `y` ; z") | |
| 396 | 398 | require.NoError(t, os.MkdirAll(base, 0o755)) | |
| 397 | 399 | 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") | ||