diff --git a/main_e2e_test.go b/main_e2e_test.go index 3a2d038..49d42e4 100644 --- a/main_e2e_test.go +++ b/main_e2e_test.go @@ -14,7 +14,6 @@ import ( "strings" "sync" "testing" - "time" "golang.org/x/net/dns/dnsmessage" @@ -33,6 +32,39 @@ func runCLI(t *testing.T, stdin string, args ...string) (int, string) { return code, stdout.String() } +// runCLICapture is runCLI with a caller-supplied stdout writer. +func runCLICapture(t *testing.T, stdout io.Writer, args ...string) int { + t.Helper() + return runWith(runArgs{args: args, stdin: strings.NewReader(""), stdout: stdout, stderr: io.Discard}) +} + +// interruptWriter captures stdout and calls fire once, after the at-th line. +// Tests interrupt on a result event instead of a wall-clock sleep, so a slow +// runner cannot finish the scan before the interrupt lands (#260). +type interruptWriter struct { + buf bytes.Buffer + at int + lines int + fire func() +} + +func newInterruptWriter(at int, fire func()) *interruptWriter { + return &interruptWriter{at: at, fire: fire} +} + +func (w *interruptWriter) Write(p []byte) (int, error) { + n, err := w.buf.Write(p) + w.lines += bytes.Count(p, []byte("\n")) + if w.lines >= w.at && w.fire != nil { + fire := w.fire + w.fire = nil + fire() + } + return n, err +} + +func (w *interruptWriter) String() string { return w.buf.String() } + // lockedBuffer is a bytes.Buffer safe for concurrent writers, standing in // for one pipe that both stdout and stderr write to. type lockedBuffer struct { @@ -842,16 +874,15 @@ func TestE2EResume(t *testing.T) { t.Fatalf("reference run found only %d names", len(want)) } - // Interrupted run: -rate 20 makes 40 names take ~2s; interrupt at ~1s. + // Interrupted run: -rate 20 makes 40 names take ~2s. Interrupt after the + // first few result lines on stdout, so the interrupt cannot miss a scan + // that a slow runner has already finished (#260). ready := make(chan struct{}) signalReady, testSignals = ready, make(chan os.Signal, 1) t.Cleanup(func() { signalReady, testSignals = nil, nil }) - go func() { - <-ready - time.Sleep(time.Second) - testSignals <- os.Interrupt - }() - code, first := runCLI(t, "", append(append([]string{}, base...), "-rate", "20", "-state", statePath, "-o", outPath, "example.com")...) + stdout := newInterruptWriter(3, func() { testSignals <- os.Interrupt }) + code := runCLICapture(t, stdout, append(append([]string{}, base...), "-rate", "20", "-state", statePath, "-o", outPath, "example.com")...) + first := stdout.String() signalReady, testSignals = nil, nil if code != 130 { t.Fatalf("interrupted run: exit %d, want 130", code) diff --git a/main_signal_unix_test.go b/main_signal_unix_test.go index bf208e8..814bf8a 100644 --- a/main_signal_unix_test.go +++ b/main_signal_unix_test.go @@ -7,7 +7,6 @@ import ( "strings" "syscall" "testing" - "time" ) // TestE2ESIGTERMExits143 covers #82: SIGTERM (container stop, systemd) exits @@ -19,17 +18,10 @@ func TestE2ESIGTERMExits143(t *testing.T) { } wl := writeFile(t, "wl.txt", words.String()) - ready := make(chan struct{}) - signalReady = ready - t.Cleanup(func() { signalReady = nil }) - go func() { - <-ready - time.Sleep(200 * time.Millisecond) // let the scan start - _ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM) - }() - - // 200 names at -rate 10 would take 20s; the signal ends it early. - code, _ := runCLI(t, "", "-simulate", "-rate", "10", "-progress=false", "-w", wl, "example.com") + // 200 names at -rate 10 would take 20s; SIGTERM after the first result + // line ends it early, without a wall-clock race (#260). + stdout := newInterruptWriter(1, func() { _ = syscall.Kill(syscall.Getpid(), syscall.SIGTERM) }) + code := runCLICapture(t, stdout, "-simulate", "-rate", "10", "-progress=false", "-w", wl, "example.com") if code != 143 { t.Fatalf("exit %d after SIGTERM, want 143", code) }