1 # Regression test for https://go.dev/issue/24050:
2 # a test that exits with an I/O stream held open
3 # should fail after a reasonable delay, not wait forever.
4 # (As of the time of writing, that delay is 10% of the timeout,
5 # but this test does not depend on its specific value.)
6
7 [short] skip 'runs a test that hangs until its WaitDelay expires'
8
9 ! go test -v -timeout=1m .
10
11 # After the test process itself prints PASS and exits,
12 # the kernel closes its stdin pipe to to the orphaned subprocess.
13 # At that point, we expect the subprocess to print 'stdin closed'
14 # and periodically log to stderr until the WaitDelay expires.
15 #
16 # Once the WaitDelay expires, the copying goroutine for 'go test' stops and
17 # closes the read side of the stderr pipe, and the subprocess will eventually
18 # exit due to a failed write to that pipe.
19
20 stdout '^--- PASS: TestOrphanCmd .*\nPASS\nstdin closed'
21 stdout '^\*\*\* Test I/O incomplete \d+.* after exiting\.\nexec: WaitDelay expired before I/O complete\nFAIL\s+example\s+\d+(\.\d+)?s'
22
23 -- go.mod --
24 module example
25
26 go 1.20
27 -- main_test.go --
28 package main
29
30 import (
31 "fmt"
32 "io"
33 "os"
34 "os/exec"
35 "testing"
36 "time"
37 )
38
39 func TestMain(m *testing.M) {
40 if os.Getenv("TEST_TIMEOUT_HANG") == "1" {
41 io.Copy(io.Discard, os.Stdin)
42 if _, err := os.Stderr.WriteString("stdin closed\n"); err != nil {
43 os.Exit(1)
44 }
45
46 ticker := time.NewTicker(100 * time.Millisecond)
47 for t := range ticker.C {
48 _, err := fmt.Fprintf(os.Stderr, "still alive at %v\n", t)
49 if err != nil {
50 os.Exit(1)
51 }
52 }
53 }
54
55 m.Run()
56 }
57
58 func TestOrphanCmd(t *testing.T) {
59 exe, err := os.Executable()
60 if err != nil {
61 t.Fatal(err)
62 }
63
64 cmd := exec.Command(exe)
65 cmd.Env = append(cmd.Environ(), "TEST_TIMEOUT_HANG=1")
66
67 // Hold stdin open until this (parent) process exits.
68 if _, err := cmd.StdinPipe(); err != nil {
69 t.Fatal(err)
70 }
71
72 // Forward stderr to the subprocess so that it can hold the stream open.
73 cmd.Stderr = os.Stderr
74
75 if err := cmd.Start(); err != nil {
76 t.Fatal(err)
77 }
78 t.Logf("started %v", cmd)
79
80 // Intentionally leak cmd when the test completes.
81 // This will allow the test process itself to exit, but (at least on Unix
82 // platforms) will keep the parent process's stderr stream open.
83 go func() {
84 if err := cmd.Wait(); err != nil {
85 os.Exit(3)
86 }
87 }()
88 }
89
View as plain text