| # Regression test for https://go.dev/issue/24050: |
| # a test that exits with an I/O stream held open |
| # should fail after a reasonable delay, not wait forever. |
| # (As of the time of writing, that delay is 10% of the timeout, |
| # but this test does not depend on its specific value.) |
| |
| [short] skip 'runs a test that hangs until its WaitDelay expires' |
| |
| ! go test -v -timeout=1m . |
| |
| # After the test process itself prints PASS and exits, |
| # the kernel closes its stdin pipe to to the orphaned subprocess. |
| # At that point, we expect the subprocess to print 'stdin closed' |
| # and periodically log to stderr until the WaitDelay expires. |
| # |
| # Once the WaitDelay expires, the copying goroutine for 'go test' stops and |
| # closes the read side of the stderr pipe, and the subprocess will eventually |
| # exit due to a failed write to that pipe. |
| |
| stdout '^--- PASS: TestOrphanCmd .*\nPASS\nstdin closed' |
| stdout '^\*\*\* Test I/O incomplete \d+.* after exiting\.\nexec: WaitDelay expired before I/O complete\nFAIL\s+example\s+\d+(\.\d+)?s' |
| |
| -- go.mod -- |
| module example |
| |
| go 1.20 |
| -- main_test.go -- |
| package main |
| |
| import ( |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "testing" |
| "time" |
| ) |
| |
| func TestMain(m *testing.M) { |
| if os.Getenv("TEST_TIMEOUT_HANG") == "1" { |
| io.Copy(io.Discard, os.Stdin) |
| if _, err := os.Stderr.WriteString("stdin closed\n"); err != nil { |
| os.Exit(1) |
| } |
| |
| ticker := time.NewTicker(100 * time.Millisecond) |
| for t := range ticker.C { |
| _, err := fmt.Fprintf(os.Stderr, "still alive at %v\n", t) |
| if err != nil { |
| os.Exit(1) |
| } |
| } |
| } |
| |
| m.Run() |
| } |
| |
| func TestOrphanCmd(t *testing.T) { |
| exe, err := os.Executable() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cmd := exec.Command(exe) |
| cmd.Env = append(cmd.Environ(), "TEST_TIMEOUT_HANG=1") |
| |
| // Hold stdin open until this (parent) process exits. |
| if _, err := cmd.StdinPipe(); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Forward stderr to the subprocess so that it can hold the stream open. |
| cmd.Stderr = os.Stderr |
| |
| if err := cmd.Start(); err != nil { |
| t.Fatal(err) |
| } |
| t.Logf("started %v", cmd) |
| |
| // Intentionally leak cmd when the test completes. |
| // This will allow the test process itself to exit, but (at least on Unix |
| // platforms) will keep the parent process's stderr stream open. |
| go func() { |
| if err := cmd.Wait(); err != nil { |
| os.Exit(3) |
| } |
| }() |
| } |