diff --git a/mfer/gpg_test.go b/mfer/gpg_test.go index 12bc342..26981b0 100644 --- a/mfer/gpg_test.go +++ b/mfer/gpg_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strconv" "strings" + "syscall" "testing" "time" @@ -425,36 +426,42 @@ func TestGPGTimeoutKillsGPG(t *testing.T) { 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 // gpg might. Killing the fake gpg leaves sleep holding its stdout and -// stderr open; the call must still return shortly after the deadline -// instead of waiting for sleep to exit. The fake gpg writes the process ID -// of sleep to a file so that the test can kill it before returning. -func TestGPGTimeoutWhenChildHoldsOutput(t *testing.T) { - pidFile := filepath.Join(t.TempDir(), "sleep.pid") +// stderr open; the call must still return once ctx ends instead of waiting +// for sleep to exit. The fake gpg writes the process ID of sleep to a named +// pipe; the test ends ctx only after reading it, so sleep is running by +// then, and kills sleep before returning. +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, - "#!/bin/sh\nsleep 3 &\necho $! >'"+pidFile+"'\nwait\n")) - t.Cleanup(func() { - pid, err := os.ReadFile(pidFile) //nolint:gosec // G304: path inside t.TempDir() - require.NoError(t, err) + "#!/bin/sh\nsleep 60 &\necho $! >'"+pidPipe+"'\nwait\n")) - n, err := strconv.Atoi(strings.TrimSpace(string(pid))) - require.NoError(t, err) - - sleep, err := os.FindProcess(n) - require.NoError(t, err) - require.NoError(t, sleep.Kill()) - }) - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() - 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") + 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) + + n, err := strconv.Atoi(strings.TrimSpace(string(pid))) + require.NoError(t, err) + + sleep, err := os.FindProcess(n) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, sleep.Kill()) }) + + cancel() + require.ErrorIs(t, <-signErr, context.Canceled) } // TestBuildPassesContextToSigning checks that a caller can cancel the gpg diff --git a/mfer/scanner_test.go b/mfer/scanner_test.go index bcdbb27..58e4605 100644 --- a/mfer/scanner_test.go +++ b/mfer/scanner_test.go @@ -304,46 +304,19 @@ func TestScannerEnumerateFS(t *testing.T) { func TestSendEnumerateStatusNonBlocking(t *testing.T) { 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) - // This should not block - done := make(chan bool) - - go func() { - 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") - } + sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1}) } func TestSendScanStatusNonBlocking(t *testing.T) { 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) - done := make(chan bool) - - go func() { - 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") - } + sendScanStatus(ch, ScanStatus{ScannedFiles: 1}) } func TestSendStatusNilChannel(t *testing.T) {