diff --git a/client/internal/certproof/helper_run_test.go b/client/internal/certproof/helper_run_test.go index 8a893f903..b3c07cfcd 100644 --- a/client/internal/certproof/helper_run_test.go +++ b/client/internal/certproof/helper_run_test.go @@ -3,6 +3,7 @@ package certproof import ( + "context" "encoding/json" "fmt" "os/exec" @@ -89,6 +90,32 @@ func TestRunHelperCmd_ReturnsWhenAGrandchildHoldsTheOutputPipe(t *testing.T) { } } +func TestRunHelperCmd_ReturnsWhenTheDeadlineKillsTheLauncher(t *testing.T) { + req := HelperRequest{Challenges: []HelperChallenge{{Nonce: []byte("asked")}}} + + // The shape the daemon actually meets on macOS: the process it launches is only a + // launcher, the work happens in a grandchild, and the deadline reaps the launcher + // while the grandchild keeps the stdout it inherited open. Unlike the case above + // the process is killed, so Wait reports that rather than ErrWaitDelay. + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", "cat >/dev/null; sleep 10 & sleep 10") + + done := make(chan error, 1) + go func() { + proofs, err := runHelperCmd(cmd, req) + assert.Empty(t, proofs, "a run the deadline cut short has nothing trustworthy to report") + done <- err + }() + + select { + case err := <-done: + require.Error(t, err, "the run was cut short, so it must not be reported as a success") + case <-time.After(5 * time.Second): + t.Fatal("runHelperCmd never returned after the deadline killed the launcher, so the collector's busy latch would stay set for the life of the daemon") + } +} + func TestRunHelperCmd_RejectsGarbage(t *testing.T) { req := HelperRequest{Challenges: []HelperChallenge{{Nonce: []byte("asked")}}}