//go:build linux || darwin || windows package subprocess import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" ) func TestRunCancellationTerminatesProcessTree(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() resultCh := make(chan runOutcome, 1) req, sentinelPath := processTreeRequest(t) go func() { result, err := Run(ctx, req) resultCh <- runOutcome{result: result, err: err} }() awaitHelperReady(t, req.EnvOverrides["SUBPROCESS_HELPER_READY_PATH"]) cancel() outcome := awaitRunOutcome(t, resultCh) if !outcome.result.Canceled { t.Fatalf("Canceled = %v, want true", outcome.result.Canceled) } if !errors.Is(outcome.err, context.Canceled) { t.Fatalf("error = %v, want context cancellation", outcome.err) } assertDescendantDidNotSurvive(t, sentinelPath) } func TestRunTimeoutTerminatesProcessTree(t *testing.T) { req, sentinelPath := processTreeRequest(t) req.Timeout = 100 * time.Millisecond result, err := Run(context.Background(), req) if !result.TimedOut { t.Fatalf("TimedOut = %v, want true", result.TimedOut) } if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("error = %v, want context deadline exceeded", err) } assertDescendantDidNotSurvive(t, sentinelPath) } func TestRunCaptureLimitTerminatesProcessTree(t *testing.T) { req, sentinelPath := processTreeRequest(t) req.Args[len(req.Args)-1] = "tree-spam" result, err := Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want capture-limit error") } if result.ExitCode == 0 { t.Fatalf("ExitCode = %d, want terminated process", result.ExitCode) } if !strings.Contains(err.Error(), "stdout diagnostic capture for subprocess exceeded") { t.Fatalf("error = %q, want stdout capture-limit context", err) } info, statErr := os.Stat(req.StdoutLogPath) if statErr != nil { t.Fatalf("stat stdout diagnostic: %v", statErr) } if info.Size() != MaxStdoutDiagnosticBytes { t.Fatalf("stdout diagnostic size = %d, want %d", info.Size(), MaxStdoutDiagnosticBytes) } assertDescendantDidNotSurvive(t, sentinelPath) } type runOutcome struct { result RunResult err error } func processTreeRequest(t *testing.T) (RunRequest, string) { t.Helper() executable, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() readyPath := filepath.Join(dir, "ready") sentinelPath := filepath.Join(dir, "descendant-survived") return RunRequest{ Executable: executable, Args: []string{"-test.run=^TestSubprocessHelper$", "--", "tree"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_READY_PATH": readyPath, "SUBPROCESS_HELPER_SENTINEL_PATH": sentinelPath, }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), }, sentinelPath } func awaitHelperReady(t *testing.T, readyPath string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(readyPath); err == nil { return } else if !errors.Is(err, os.ErrNotExist) { t.Fatalf("stat helper readiness: %v", err) } time.Sleep(10 * time.Millisecond) } t.Fatal("helper did not start its descendant") } func awaitRunOutcome(t *testing.T, outcomes <-chan runOutcome) runOutcome { t.Helper() select { case outcome := <-outcomes: if outcome.err == nil { t.Fatal("Run() error = nil, want cancellation error") } return outcome case <-time.After(3 * time.Second): t.Fatal("Run() did not return after cancellation") return runOutcome{} } } func assertDescendantDidNotSurvive(t *testing.T, sentinelPath string) { t.Helper() time.Sleep(700 * time.Millisecond) if _, err := os.Stat(sentinelPath); err == nil { t.Fatal("descendant survived cancellation and wrote its sentinel") } else if !errors.Is(err, os.ErrNotExist) { t.Fatalf("stat descendant sentinel: %v", err) } }