- while a view runs, runLive relays SIGINT and SIGTERM (unless ignored); on a signal it kills the program, which shows the cursor and restores the terminal, unregisters the view, stops relaying and raises the same signal again, so the default action or the command's own handler still applies - the generated main, bonfire.Execute's context and Ctrl+C for commands without a live view are unchanged - TestLiveViewRestoresTerminalOnSignal pins both paths in a re-executed helper process (WR-01)
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
//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())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|