Scanner status tests no longer depend on a 100 ms timer (closes #124)
check / check (push) Waiting to run

The two status-send tests now call the send directly on a channel nobody
receives from, so a blocking send hangs the test into its timeout instead
of racing a 100 ms timer that a loaded host can miss.

The gpg test for a child holding gpg's output had the same problem: its
100 ms deadline could fire before the fake gpg wrote the PID of sleep,
and the cleanup then failed. The fake gpg now writes that PID to a named
pipe, and the test cancels only after reading it; sleep runs past the
test timeout, so a call that waits for it fails without a time check.

Model: opus-5-5
This commit is contained in:
2026-10-04 06:03:11 +00:00
parent a2732cf8da
commit 3f1524020d
2 changed files with 35 additions and 55 deletions
+26 -19
View File
@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@@ -425,18 +426,31 @@ func TestGPGTimeoutKillsGPG(t *testing.T) {
assert.Contains(t, err.Error(), "gpg sign failed: gpg timed out") assert.Contains(t, err.Error(), "gpg sign failed: gpg timed out")
} }
// TestGPGTimeoutWhenChildHoldsOutput uses a fake gpg that runs sleep as a // TestGPGCancelWhenChildHoldsOutput uses a fake gpg that runs sleep as a
// child instead of exec-ing it, the way a wrapper script around the real // child instead of exec-ing it, the way a wrapper script around the real
// gpg might. Killing the fake gpg leaves sleep holding its stdout and // gpg might. Killing the fake gpg leaves sleep holding its stdout and
// stderr open; the call must still return shortly after the deadline // stderr open; the call must still return once ctx ends instead of waiting
// instead of waiting for sleep to exit. The fake gpg writes the process ID // for sleep to exit. The fake gpg writes the process ID of sleep to a named
// of sleep to a file so that the test can kill it before returning. // pipe; the test ends ctx only after reading it, so sleep is running by
func TestGPGTimeoutWhenChildHoldsOutput(t *testing.T) { // then, and kills sleep before returning.
pidFile := filepath.Join(t.TempDir(), "sleep.pid") func TestGPGCancelWhenChildHoldsOutput(t *testing.T) {
pidPipe := filepath.Join(t.TempDir(), "sleep.pid")
require.NoError(t, syscall.Mkfifo(pidPipe, 0o600))
// sleep outlasts the 30 s test timeout, so a call that waits for it fails.
t.Setenv("PATH", fakeGPGPath(t, t.Setenv("PATH", fakeGPGPath(t,
"#!/bin/sh\nsleep 3 &\necho $! >'"+pidFile+"'\nwait\n")) "#!/bin/sh\nsleep 60 &\necho $! >'"+pidPipe+"'\nwait\n"))
t.Cleanup(func() {
pid, err := os.ReadFile(pidFile) //nolint:gosec // G304: path inside t.TempDir() ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signErr := make(chan error, 1)
go func() {
_, err := gpgSign(ctx, []byte("data"), GPGKeyID("any"))
signErr <- err
}()
pid, err := os.ReadFile(pidPipe) //nolint:gosec // G304: path inside t.TempDir()
require.NoError(t, err) require.NoError(t, err)
n, err := strconv.Atoi(strings.TrimSpace(string(pid))) n, err := strconv.Atoi(strings.TrimSpace(string(pid)))
@@ -444,17 +458,10 @@ func TestGPGTimeoutWhenChildHoldsOutput(t *testing.T) {
sleep, err := os.FindProcess(n) sleep, err := os.FindProcess(n)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, sleep.Kill()) t.Cleanup(func() { require.NoError(t, sleep.Kill()) })
})
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) cancel()
defer cancel() require.ErrorIs(t, <-signErr, context.Canceled)
start := time.Now()
_, err := gpgSign(ctx, []byte("data"), GPGKeyID("any"))
require.ErrorIs(t, err, context.DeadlineExceeded)
assert.Less(t, time.Since(start), 3*time.Second,
"the call waited for the child holding gpg's output to exit")
} }
// TestBuildPassesContextToSigning checks that a caller can cancel the gpg // TestBuildPassesContextToSigning checks that a caller can cancel the gpg
+2 -29
View File
@@ -304,46 +304,19 @@ func TestScannerEnumerateFS(t *testing.T) {
func TestSendEnumerateStatusNonBlocking(t *testing.T) { func TestSendEnumerateStatusNonBlocking(t *testing.T) {
t.Parallel() t.Parallel()
// Channel with no buffer - send should not block // Nobody receives, so a blocking send would hang the test into its timeout.
ch := make(chan EnumerateStatus) ch := make(chan EnumerateStatus)
// This should not block
done := make(chan bool)
go func() {
sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1}) sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1})
done <- true
}()
select {
case <-done:
// Success - did not block
case <-time.After(100 * time.Millisecond):
t.Fatal("sendEnumerateStatus blocked on full channel")
}
} }
func TestSendScanStatusNonBlocking(t *testing.T) { func TestSendScanStatusNonBlocking(t *testing.T) {
t.Parallel() t.Parallel()
// Channel with no buffer - send should not block // Nobody receives, so a blocking send would hang the test into its timeout.
ch := make(chan ScanStatus) ch := make(chan ScanStatus)
done := make(chan bool)
go func() {
sendScanStatus(ch, ScanStatus{ScannedFiles: 1}) sendScanStatus(ch, ScanStatus{ScannedFiles: 1})
done <- true
}()
select {
case <-done:
// Success - did not block
case <-time.After(100 * time.Millisecond):
t.Fatal("sendScanStatus blocked on full channel")
}
} }
func TestSendStatusNilChannel(t *testing.T) { func TestSendStatusNilChannel(t *testing.T) {