From 837f947473605fab74fa164c615b07139e2f41af Mon Sep 17 00:00:00 2001 From: Noah Khomer <108771853+noahkhomer18@users.noreply.github.com> Date: Sat, 14 Nov 2026 09:05:04 -0600 Subject: [PATCH] test: fire interrupt tests from an event, not a sleep Both interrupt tests decided when to interrupt with a fixed wall-clock sleep. A slow or loaded runner could finish the scan first, which is the flake class this repo has been clearing out. Interrupts now fire from an interruptWriter that triggers on the Nth stdout line, so the signal lands while the scan is genuinely running. No time.Sleep decides the interrupt in either file. Closes #260 --- main_e2e_test.go | 47 +++++++++++++++++++++++++++++++++------- main_signal_unix_test.go | 16 ++++---------- 2 files changed, 43 insertions(+), 20 deletions(-) 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) }