From 6ecd843e7ceaeff11ec2feb1709fdbb34a7c334b Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Wed, 22 Nov 2017 07:13:25 -0800 Subject: [PATCH] os/signal: make TestTerminalSignal more reliable Look for program output and shell prompt to see when to continue. Updates #22845 Change-Id: I44ed1908861f3b0dc098aee9a401324b77268921 Reviewed-on: https://go-review.googlesource.com/79395 Run-TryBot: Ian Lance Taylor TryBot-Result: Gobot Gobot Reviewed-by: Michael Munday Reviewed-by: Brad Fitzpatrick --- src/os/signal/signal_cgo_test.go | 101 +++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/src/os/signal/signal_cgo_test.go b/src/os/signal/signal_cgo_test.go index 6eed979ab0..0daaacb07c 100644 --- a/src/os/signal/signal_cgo_test.go +++ b/src/os/signal/signal_cgo_test.go @@ -9,6 +9,7 @@ package signal_test import ( "bufio" + "bytes" "context" "fmt" "io" @@ -23,11 +24,21 @@ import ( ) func TestTerminalSignal(t *testing.T) { + const enteringRead = "test program entering read" if os.Getenv("GO_TEST_TERMINAL_SIGNALS") != "" { var b [1]byte - fmt.Println("entering read") + fmt.Println(enteringRead) n, err := os.Stdin.Read(b[:]) - fmt.Printf("read %d bytes: %q\n", n, b) + if n == 1 { + if b[0] == '\n' { + // This is what we expect + fmt.Println("read newline") + } else { + fmt.Printf("read 1 byte: %q\n", b) + } + } else { + fmt.Printf("read %d bytes\n", n) + } if err != nil { fmt.Println(err) os.Exit(1) @@ -35,6 +46,8 @@ func TestTerminalSignal(t *testing.T) { os.Exit(0) } + t.Parallel() + // The test requires a shell that uses job control. bash, err := exec.LookPath("bash") if err != nil { @@ -47,6 +60,8 @@ func TestTerminalSignal(t *testing.T) { scale = sc } } + pause := time.Duration(scale) * 10 * time.Millisecond + wait := time.Duration(scale) * 5 * time.Second // The test only fails when using a "slow device," in this // case a pseudo-terminal. @@ -65,7 +80,7 @@ func TestTerminalSignal(t *testing.T) { // Start an interactive shell. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, bash, "-i") + cmd := exec.CommandContext(ctx, bash, "--norc", "--noprofile", "-i") cmd.Stdin = slave cmd.Stdout = slave cmd.Stderr = slave @@ -83,15 +98,20 @@ func TestTerminalSignal(t *testing.T) { t.Errorf("closing slave: %v", err) } + progReady := make(chan bool) + sawPrompt := make(chan bool, 10) + const prompt = "prompt> " + // Read data from master in the background. go func() { - buf := bufio.NewReader(master) + input := bufio.NewReader(master) + var line, handled []byte for { - data, err := buf.ReadBytes('\n') - if len(data) > 0 { - t.Logf("%q", data) - } + b, err := input.ReadByte() if err != nil { + if len(line) > 0 || len(handled) > 0 { + t.Logf("%q", append(handled, line...)) + } if perr, ok := err.(*os.PathError); ok { err = perr.Err } @@ -103,37 +123,96 @@ func TestTerminalSignal(t *testing.T) { } return } + + line = append(line, b) + + if b == '\n' { + t.Logf("%q", append(handled, line...)) + line = nil + handled = nil + continue + } + + if bytes.Contains(line, []byte(enteringRead)) { + close(progReady) + handled = append(handled, line...) + line = nil + } else if bytes.Contains(line, []byte(prompt)) && !bytes.Contains(line, []byte("PS1=")) { + sawPrompt <- true + handled = append(handled, line...) + line = nil + } } }() + // Set the bash prompt so that we can see it. + if _, err := master.Write([]byte("PS1='" + prompt + "'\n")); err != nil { + t.Fatal("setting prompt: %v", err) + } + select { + case <-sawPrompt: + case <-time.After(wait): + t.Fatal("timed out waiting for shell prompt") + } + // Start a small program that reads from stdin // (namely the code at the top of this function). if _, err := master.Write([]byte("GO_TEST_TERMINAL_SIGNALS=1 " + os.Args[0] + " -test.run=TestTerminalSignal\n")); err != nil { t.Fatal(err) } + // Wait for the program to print that it is starting. + select { + case <-progReady: + case <-time.After(wait): + t.Fatal("timed out waiting for program to start") + } + // Give the program time to enter the read call. - time.Sleep(time.Duration(scale) * 100 * time.Millisecond) + // It doesn't matter much if we occasionally don't wait long enough; + // we won't be testing what we want to test, but the overall test + // will pass. + time.Sleep(pause) // Send a ^Z to stop the program. if _, err := master.Write([]byte{26}); err != nil { t.Fatalf("writing ^Z to pty: %v", err) } - // Give the process time to handle the signal. - time.Sleep(time.Duration(scale) * 100 * time.Millisecond) + // Wait for the program to stop and return to the shell. + select { + case <-sawPrompt: + case <-time.After(wait): + t.Fatal("timed out waiting for shell prompt") + } // Restart the stopped program. if _, err := master.Write([]byte("fg\n")); err != nil { t.Fatalf("writing %q to pty: %v", "fg", err) } + // Give the process time to restart. + // This is potentially racy: if the process does not restart + // quickly enough then the byte we send will go to bash rather + // than the program. Unfortunately there isn't anything we can + // look for to know that the program is running again. + // bash will print the program name, but that happens before it + // restarts the program. + time.Sleep(10 * pause) + // Write some data for the program to read, // which should cause it to exit. if _, err := master.Write([]byte{'\n'}); err != nil { t.Fatalf("writing %q to pty: %v", "\n", err) } + // Wait for the program to exit. + select { + case <-sawPrompt: + case <-time.After(wait): + t.Fatal("timed out waiting for shell prompt") + } + // Exit the shell with the program's exit status. if _, err := master.Write([]byte("exit $?\n")); err != nil { t.Fatalf("writing %q to pty: %v", "exit", err)