//go:build unix package bonfire import ( "context" "errors" "fmt" "os" "os/exec" "os/signal" "strings" "syscall" "testing" "time" ) const ( hideCursor = "\x1b[?25l" showCursor = "\x1b[?25h" ) // TestLiveSignalHelper is the helper process of // TestLiveViewRestoresTerminalOnSignal: with BONFIRE_LIVE_SIGNAL_HELPER set // it runs a spinner on a terminal console over its stdout, prints ready on // stderr and waits. In own_handler mode it also holds its own signal // context and exits 0 once that context ends. func TestLiveSignalHelper(t *testing.T) { mode := os.Getenv("BONFIRE_LIVE_SIGNAL_HELPER") if mode == "" { return } ctx := context.Background() if mode == "own_handler" { var stop context.CancelFunc ctx, stop = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() } c := newConsole(strings.NewReader(""), os.Stdout, os.Stderr, noEnv, boolPtr(true), boolPtr(true)) _ = c.Spinner("waiting for a signal", func() error { fmt.Fprintln(os.Stderr, "ready") select { case <-ctx.Done(): case <-time.After(20 * time.Second): } return nil }) if mode == "own_handler" && ctx.Err() != nil { os.Exit(0) } os.Exit(3) } // TestLiveViewRestoresTerminalOnSignal (WR-01): SIGINT during a live view // shows the cursor again and then still ends the process by the signal, // or reaches the command's own signal handler when it has one. func TestLiveViewRestoresTerminalOnSignal(t *testing.T) { for _, mode := range []string{"default", "own_handler"} { t.Run(mode, func(t *testing.T) { var stdout, stderr lockedBuffer cmd := exec.Command(os.Args[0], "-test.run=^TestLiveSignalHelper$") cmd.Env = append(os.Environ(), "BONFIRE_LIVE_SIGNAL_HELPER="+mode) cmd.Stdout, cmd.Stderr = &stdout, &stderr if err := cmd.Start(); err != nil { t.Fatal(err) } deadline := time.Now().Add(10 * time.Second) for !strings.Contains(stderr.String(), "ready") || !strings.Contains(stdout.String(), hideCursor) { if time.Now().After(deadline) { _ = cmd.Process.Kill() t.Fatalf("helper not ready; stdout %q, stderr %q", stdout.String(), stderr.String()) } time.Sleep(10 * time.Millisecond) } if err := cmd.Process.Signal(os.Interrupt); err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- cmd.Wait() }() var err error select { case err = <-done: case <-time.After(10 * time.Second): _ = cmd.Process.Kill() t.Fatalf("helper did not end after SIGINT; stdout %q", stdout.String()) } out := stdout.String() if i := strings.LastIndex(out, hideCursor); !strings.Contains(out[i:], showCursor) { t.Errorf("cursor left hidden: no show-cursor after the last hide-cursor in %q", out) } switch mode { case "default": var ee *exec.ExitError if !errors.As(err, &ee) { t.Fatalf("helper exited cleanly (%v), want death by SIGINT", err) } ws, ok := ee.Sys().(syscall.WaitStatus) if !ok || !ws.Signaled() || ws.Signal() != syscall.SIGINT { t.Errorf("helper ended with %v, want death by SIGINT", err) } case "own_handler": if err != nil { t.Errorf("helper with its own handler ended with %v, want exit 0; stderr %q", err, stderr.String()) } } }) } }