# 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) } }() }