package subprocess import ( "bytes" "context" "errors" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "testing" "time" "gopkg.in/yaml.v3" ) func TestRunSuccessCapturesStdoutStderr(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() stdoutPath := filepath.Join(dir, "stdout.log") stderrPath := filepath.Join(dir, "stderr.log") req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "success"}, EnvOverrides: map[string]string{"GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_STDOUT": "hello-out", "SUBPROCESS_HELPER_STDERR": "hello-err"}, StdoutLogPath: stdoutPath, StderrLogPath: stderrPath, } res, err := Run(context.Background(), req) if err != nil { t.Fatalf("Run() error = %v", err) } if res.ExitCode != 0 { t.Fatalf("ExitCode = %d, want 0", res.ExitCode) } stdoutBytes, err := os.ReadFile(stdoutPath) if err != nil { t.Fatalf("read stdout log: %v", err) } if !strings.Contains(string(stdoutBytes), "hello-out") { t.Fatalf("stdout log = %q, want hello-out", string(stdoutBytes)) } stderrBytes, err := os.ReadFile(stderrPath) if err != nil { t.Fatalf("read stderr log: %v", err) } if !strings.Contains(string(stderrBytes), "hello-err") { t.Fatalf("stderr log = %q, want hello-err", string(stderrBytes)) } } func TestRunFailureReturnsUsefulError(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() stdoutPath := filepath.Join(dir, "stdout.log") stderrPath := filepath.Join(dir, "stderr.log") req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "fail"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", }, StdoutLogPath: stdoutPath, StderrLogPath: stderrPath, } res, err := Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want non-nil") } if res.ExitCode == 0 { t.Fatalf("ExitCode = %d, want non-zero", res.ExitCode) } if !strings.Contains(err.Error(), "exit code") { t.Fatalf("error = %q, want exit code context", err.Error()) } if !strings.Contains(err.Error(), exe) { t.Fatalf("error = %q, want executable context", err.Error()) } if !strings.Contains(err.Error(), stdoutPath) || !strings.Contains(err.Error(), stderrPath) { t.Fatalf("error = %q, want stdout/stderr log paths", err.Error()) } } func TestRunFailureRedactsSensitiveTail(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } secretValue := "super-secret-value" dir := t.TempDir() req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "failsecret"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "API_KEY": secretValue, "SUBPROCESS_HELPER_ENV_KEY": "API_KEY", }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), } _, err = Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want non-nil") } if strings.Contains(err.Error(), secretValue) { t.Fatalf("error leaked secret value: %q", err.Error()) } if !strings.Contains(err.Error(), "") { t.Fatalf("error = %q, want redacted stderr tail marker", err.Error()) } for _, path := range []string{req.StdoutLogPath, req.StderrLogPath} { data, readErr := os.ReadFile(path) if readErr != nil { t.Fatalf("read diagnostic %q: %v", path, readErr) } if strings.Contains(string(data), secretValue) { t.Fatalf("diagnostic %q leaked secret: %q", path, data) } } } func TestRunRejectsSymlinkDiagnosticWithoutTruncatingTarget(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("creating symlinks requires privileges that are not available on every Windows runner") } exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() targetPath := filepath.Join(dir, "outside.log") const original = "must remain unchanged" if err := os.WriteFile(targetPath, []byte(original), 0o600); err != nil { t.Fatalf("WriteFile(target) error = %v", err) } stdoutPath := filepath.Join(dir, "stdout.log") if err := os.Symlink(targetPath, stdoutPath); err != nil { t.Fatalf("Symlink() error = %v", err) } _, err = Run(context.Background(), RunRequest{ Executable: exe, Args: []string{"-test.run=^TestSubprocessHelper$", "--", "success"}, EnvOverrides: map[string]string{"GO_WANT_SUBPROCESS_HELPER": "1"}, StdoutLogPath: stdoutPath, StderrLogPath: filepath.Join(dir, "stderr.log"), }) if err == nil || !strings.Contains(err.Error(), "symbolic link") { t.Fatalf("Run() error = %v, want symbolic-link rejection", err) } data, readErr := os.ReadFile(targetPath) if readErr != nil { t.Fatalf("ReadFile(target) error = %v", readErr) } if string(data) != original { t.Fatalf("target content = %q, want %q", data, original) } } func TestRunFailureUsesOpenedDiagnosticAfterPathReplacement(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() readyPath := filepath.Join(dir, "ready") releasePath := filepath.Join(dir, "release") stderrPath := filepath.Join(dir, "stderr.log") openedPath := filepath.Join(dir, "opened-stderr.log") const secretValue = "replacement-api-key-value" const commandContent = "trusted command failure" req := RunRequest{ Executable: exe, Args: []string{"-test.run=^TestSubprocessHelper$", "--", "delayed-fail"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "API_KEY": secretValue, "SUBPROCESS_HELPER_READY_PATH": readyPath, "SUBPROCESS_HELPER_RELEASE_PATH": releasePath, "SUBPROCESS_HELPER_STDERR": commandContent, }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: stderrPath, } resultCh := make(chan error, 1) go func() { _, runErr := Run(context.Background(), req) resultCh <- runErr }() waitForHelperFile(t, readyPath) if err := os.Rename(stderrPath, openedPath); err != nil { t.Fatalf("Rename(stderr log) error = %v", err) } if err := os.WriteFile(stderrPath, []byte(secretValue), 0o600); err != nil { t.Fatalf("WriteFile(replacement) error = %v", err) } if err := os.WriteFile(releasePath, []byte("continue"), 0o600); err != nil { t.Fatalf("WriteFile(release) error = %v", err) } select { case runErr := <-resultCh: if runErr == nil { t.Fatal("Run() error = nil, want command failure") } if strings.Contains(runErr.Error(), secretValue) { t.Fatalf("error read replacement-path content: %q", runErr) } if !strings.Contains(runErr.Error(), commandContent) { t.Fatalf("error = %q, want retained command diagnostic", runErr) } case <-time.After(3 * time.Second): t.Fatal("Run() did not return after helper release") } openedData, err := os.ReadFile(openedPath) if err != nil { t.Fatalf("ReadFile(opened diagnostic) error = %v", err) } if !strings.Contains(string(openedData), commandContent) { t.Fatalf("opened diagnostic = %q, want command content", openedData) } replacementData, err := os.ReadFile(stderrPath) if err != nil { t.Fatalf("ReadFile(replacement diagnostic) error = %v", err) } if string(replacementData) != secretValue { t.Fatalf("replacement diagnostic = %q, want %q", replacementData, secretValue) } } func TestRunRedactsSplitCredentialInSeparateAndSharedDiagnostics(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } const secretValue = "split-super-secret-value" for _, shared := range []bool{false, true} { t.Run(map[bool]string{false: "separate", true: "shared"}[shared], func(t *testing.T) { dir := t.TempDir() stdoutPath := filepath.Join(dir, "stdout.log") stderrPath := filepath.Join(dir, "stderr.log") if shared { stderrPath = stdoutPath } req := RunRequest{ Executable: exe, Args: []string{"-test.run=^TestSubprocessHelper$", "--", "splitsecret"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "API_KEY": secretValue, }, StdoutLogPath: stdoutPath, StderrLogPath: stderrPath, } _, runErr := Run(context.Background(), req) if runErr == nil { t.Fatal("Run() error = nil, want command failure") } if strings.Contains(runErr.Error(), secretValue) || !strings.Contains(runErr.Error(), "") { t.Fatalf("error = %q, want redacted credential", runErr) } paths := map[string]struct{}{stdoutPath: {}, stderrPath: {}} for path := range paths { data, readErr := os.ReadFile(path) if readErr != nil { t.Fatalf("ReadFile(%q) error = %v", path, readErr) } if strings.Contains(string(data), secretValue) || !strings.Contains(string(data), "") { t.Fatalf("diagnostic %q = %q, want redacted credential", path, data) } } }) } } func TestRunRedactsInheritedSensitiveEnvironment(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } secretValue := "inherited-secret-value" t.Setenv("OPENROUTER_API_KEY", secretValue) dir := t.TempDir() req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "echoenv"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_ENV_KEY": "OPENROUTER_API_KEY", }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), } _, err = Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want non-nil") } if strings.Contains(err.Error(), secretValue) { t.Fatalf("error leaked inherited secret: %q", err) } for _, path := range []string{req.StdoutLogPath, req.StderrLogPath} { data, readErr := os.ReadFile(path) if readErr != nil { t.Fatalf("read diagnostic %q: %v", path, readErr) } if strings.Contains(string(data), secretValue) { t.Fatalf("diagnostic %q leaked inherited secret: %q", path, data) } } } func TestRunRedactsSensitiveOutputAndErrorTail(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } secretValue := "override-secret-value" dir := t.TempDir() req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "echoenv"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_ENV_KEY": "OPENROUTER_API_KEY", "OPENROUTER_API_KEY": secretValue, }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), } _, err = Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want non-nil") } if strings.Contains(err.Error(), secretValue) || !strings.Contains(err.Error(), "") { t.Fatalf("error = %q, want redacted secret", err) } for _, path := range []string{req.StdoutLogPath, req.StderrLogPath} { data, readErr := os.ReadFile(path) if readErr != nil { t.Fatalf("read diagnostic %q: %v", path, readErr) } if strings.Contains(string(data), secretValue) || !strings.Contains(string(data), "") { t.Fatalf("diagnostic %q = %q, want redacted secret", path, data) } } } func TestStreamRedactorHandlesSplitAndOverlappingSecrets(t *testing.T) { redactor := newStreamRedactor([]string{"abc", "abcde", "cde", ""}) var output bytes.Buffer output.Write(redactor.Write([]byte("start-ab"))) output.Write(redactor.Write([]byte("cde-end"))) output.Write(redactor.Flush()) if got := output.String(); got != "start--end" { t.Fatalf("redacted output = %q, want one redacted marker", got) } } func TestDiagnosticWriterHonorsExactLimitAndCapPlusOne(t *testing.T) { exactLogs := &logWriters{limits: make(chan *captureLimitError, 1)} var exactOutput bytes.Buffer exact := newDiagnosticWriter(exactLogs, "stdout", "test", &exactOutput, 5, nil) if _, err := exact.Write([]byte("abcde")); err != nil { t.Fatalf("exact Write() error = %v", err) } if err := exact.Flush(); err != nil { t.Fatalf("exact Flush() error = %v", err) } if got := exactOutput.String(); got != "abcde" { t.Fatalf("exact output = %q, want abcde", got) } if exactLogs.Limit() != nil { t.Fatal("exact write recorded a capture limit") } cappedLogs := &logWriters{limits: make(chan *captureLimitError, 1)} var cappedOutput bytes.Buffer capped := newDiagnosticWriter(cappedLogs, "stderr", "test", &cappedOutput, 5, nil) if _, err := capped.Write([]byte("abcdef")); err == nil { t.Fatal("cap-plus-one Write() error = nil, want capture limit") } if err := capped.Flush(); err != nil { t.Fatalf("cap-plus-one Flush() error = %v", err) } if got := cappedOutput.String(); got != "abcde" { t.Fatalf("capped output = %q, want abcde", got) } if limit := cappedLogs.Limit(); limit == nil || limit.stream != "stderr" || limit.limit != 5 { t.Fatalf("capture limit = %#v, want stderr limit 5", limit) } } func TestDiagnosticWriterRetainsBoundedRedactedTail(t *testing.T) { logs := &logWriters{limits: make(chan *captureLimitError, 1)} var output bytes.Buffer secret := "credential-value" writer := newDiagnosticWriter(logs, "stderr", "test", &output, 16*1024, []string{secret}) prefix := strings.Repeat("x", diagnosticTailBytes+512) if _, err := writer.Write([]byte(prefix + secret[:7])); err != nil { t.Fatalf("first Write() error = %v", err) } if _, err := writer.Write([]byte(secret[7:] + "-failure")); err != nil { t.Fatalf("second Write() error = %v", err) } if err := writer.Flush(); err != nil { t.Fatalf("Flush() error = %v", err) } tail := writer.Tail() if len(tail) > diagnosticTailBytes { t.Fatalf("retained tail length = %d, want at most %d", len(tail), diagnosticTailBytes) } if strings.Contains(tail, secret) || !strings.Contains(tail, "-failure") { t.Fatalf("retained tail = %q, want bounded redacted content", tail) } } func TestRunFailureAddsBadDescriptorHint(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "failbadfd"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", }, StdoutLogPath: filepath.Join(dir, "stdout.log"), StderrLogPath: filepath.Join(dir, "stderr.log"), } _, err = Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want non-nil") } if !strings.Contains(err.Error(), "Bad file descriptor") { t.Fatalf("error = %q, want stderr tail content", err.Error()) } if !strings.Contains(err.Error(), "stderr stream appears invalid in child process") { t.Fatalf("error = %q, want bad-descriptor hint", err.Error()) } } func TestRunTimeout(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "sleep"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", }, Timeout: 50 * time.Millisecond, } res, err := Run(context.Background(), req) if err == nil { t.Fatal("Run() error = nil, want timeout error") } if !res.TimedOut { t.Fatalf("TimedOut = %v, want true", res.TimedOut) } if !strings.Contains(err.Error(), "timed out") { t.Fatalf("error = %q, want timeout context", err.Error()) } } func TestRunInheritsParentEnvironmentByDefault(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } t.Setenv("PATH", "inherited-value") dir := t.TempDir() stdoutPath := filepath.Join(dir, "stdout.log") req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "printenv"}, EnvOverrides: map[string]string{"GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_ENV_KEY": "PATH"}, StdoutLogPath: stdoutPath, } if _, err := Run(context.Background(), req); err != nil { t.Fatalf("Run() error = %v", err) } data, err := os.ReadFile(stdoutPath) if err != nil { t.Fatalf("read stdout log: %v", err) } if strings.TrimSpace(string(data)) != "inherited-value" { t.Fatalf("stdout = %q, want inherited-value", strings.TrimSpace(string(data))) } } func TestRunEnvOverridesWinOverInheritedValues(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() stdoutPath := filepath.Join(dir, "stdout.log") req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "printenv"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_ENV_KEY": "SUBPROCESS_PARENT_VALUE", "SUBPROCESS_PARENT_VALUE": "override-value", }, StdoutLogPath: stdoutPath, } if _, err := Run(context.Background(), req); err != nil { t.Fatalf("Run() error = %v", err) } data, err := os.ReadFile(stdoutPath) if err != nil { t.Fatalf("read stdout log: %v", err) } if strings.TrimSpace(string(data)) != "override-value" { t.Fatalf("stdout = %q, want override-value", strings.TrimSpace(string(data))) } } func TestRunSharedStdoutStderrLogPath(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } dir := t.TempDir() sharedLogPath := filepath.Join(dir, "shared.log") req := RunRequest{ Executable: exe, Args: []string{"-test.run=TestSubprocessHelper", "--", "success"}, EnvOverrides: map[string]string{ "GO_WANT_SUBPROCESS_HELPER": "1", "SUBPROCESS_HELPER_STDOUT": "shared-out", "SUBPROCESS_HELPER_STDERR": "shared-err", }, StdoutLogPath: sharedLogPath, StderrLogPath: sharedLogPath, } res, err := Run(context.Background(), req) if err != nil { t.Fatalf("Run() error = %v", err) } if res.ExitCode != 0 { t.Fatalf("ExitCode = %d, want 0", res.ExitCode) } data, err := os.ReadFile(sharedLogPath) if err != nil { t.Fatalf("read shared log: %v", err) } text := string(data) if !strings.Contains(text, "shared-out") || !strings.Contains(text, "shared-err") { t.Fatalf("shared log = %q, want both stdout and stderr content", text) } } func TestWriteYAMLAtomic(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.generated.yml") if err := WriteYAMLAtomic(path, map[string]any{"name": "narratio", "stage": "merge"}, 0o644); err != nil { t.Fatalf("WriteYAMLAtomic() error = %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read yaml: %v", err) } var got map[string]any if err := yaml.Unmarshal(data, &got); err != nil { t.Fatalf("yaml unmarshal: %v", err) } if got["name"] != "narratio" { t.Fatalf("name = %#v, want narratio", got["name"]) } } func TestWriteYAMLAtomicOverwriteNoTempResidue(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.generated.yml") if err := WriteYAMLAtomic(path, map[string]any{"value": "one"}, 0o644); err != nil { t.Fatalf("first write: %v", err) } if err := WriteYAMLAtomic(path, map[string]any{"value": "two"}, 0o644); err != nil { t.Fatalf("second write: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read yaml: %v", err) } if !strings.Contains(string(data), "two") { t.Fatalf("yaml = %q, want overwritten value", string(data)) } entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("ReadDir() error = %v", err) } for _, entry := range entries { name := entry.Name() if strings.Contains(name, ".tmp-") { t.Fatalf("temp file residue found: %q", name) } } } func TestSubprocessHelper(t *testing.T) { if os.Getenv("GO_WANT_SUBPROCESS_HELPER") != "1" { return } args := os.Args mode := "" for i := range args { if args[i] == "--" && i+1 < len(args) { mode = args[i+1] break } } if mode == "" { os.Exit(2) } switch mode { case "success": _, _ = os.Stdout.WriteString(os.Getenv("SUBPROCESS_HELPER_STDOUT") + "\n") _, _ = os.Stderr.WriteString(os.Getenv("SUBPROCESS_HELPER_STDERR") + "\n") os.Exit(0) case "fail": _, _ = os.Stderr.WriteString("intentional failure\n") os.Exit(3) case "failsecret": key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY") _, _ = os.Stderr.WriteString("secret:" + os.Getenv(key) + "\n") os.Exit(4) case "failbadfd": _, _ = os.Stderr.WriteString("OSError: [Errno 9] Bad file descriptor\n") os.Exit(120) case "delayed-fail": if err := os.WriteFile(os.Getenv("SUBPROCESS_HELPER_READY_PATH"), []byte("ready"), 0o600); err != nil { os.Exit(4) } deadline := time.Now().Add(2 * time.Second) for { if _, err := os.Stat(os.Getenv("SUBPROCESS_HELPER_RELEASE_PATH")); err == nil { break } else if !errors.Is(err, os.ErrNotExist) || time.Now().After(deadline) { os.Exit(5) } time.Sleep(10 * time.Millisecond) } _, _ = os.Stderr.WriteString(os.Getenv("SUBPROCESS_HELPER_STDERR")) os.Exit(6) case "splitsecret": secret := os.Getenv("API_KEY") split := len(secret) / 2 for _, stream := range []*os.File{os.Stdout, os.Stderr} { _, _ = stream.WriteString(secret[:split]) time.Sleep(20 * time.Millisecond) _, _ = stream.WriteString(secret[split:] + "\n") } os.Exit(7) case "sleep": time.Sleep(500 * time.Millisecond) os.Exit(0) case "printenv": key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY") _, _ = os.Stdout.WriteString(os.Getenv(key) + "\n") os.Exit(0) case "echoenv": key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY") value := os.Getenv(key) _, _ = os.Stdout.WriteString(value) _, _ = os.Stderr.WriteString(value) os.Exit(5) case "spam": chunk := strings.Repeat("x", 64*1024) count, _ := strconv.Atoi(os.Getenv("SUBPROCESS_HELPER_CHUNKS")) for range count { _, _ = os.Stdout.WriteString(chunk) } os.Exit(0) case "tree-spam": descendant := exec.Command(os.Args[0], "-test.run=^TestSubprocessHelper$", "--", "descendant") descendant.Env = append(os.Environ(), "GO_WANT_SUBPROCESS_HELPER=1") descendant.Stdout = os.Stdout descendant.Stderr = os.Stderr if err := descendant.Start(); err != nil { os.Exit(3) } if err := os.WriteFile(os.Getenv("SUBPROCESS_HELPER_READY_PATH"), []byte("ready"), 0o600); err != nil { os.Exit(4) } chunk := strings.Repeat("x", 64*1024) for { _, _ = os.Stdout.WriteString(chunk) } case "tree": descendant := exec.Command(os.Args[0], "-test.run=^TestSubprocessHelper$", "--", "descendant") descendant.Env = append(os.Environ(), "GO_WANT_SUBPROCESS_HELPER=1") descendant.Stdout = os.Stdout descendant.Stderr = os.Stderr if err := descendant.Start(); err != nil { os.Exit(3) } if err := os.WriteFile(os.Getenv("SUBPROCESS_HELPER_READY_PATH"), []byte("ready"), 0o600); err != nil { os.Exit(4) } time.Sleep(10 * time.Second) os.Exit(0) case "descendant": time.Sleep(500 * time.Millisecond) _ = os.WriteFile(os.Getenv("SUBPROCESS_HELPER_SENTINEL_PATH"), []byte("survived"), 0o600) time.Sleep(10 * time.Second) os.Exit(0) default: os.Exit(2) } } func waitForHelperFile(t *testing.T, path string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(path); err == nil { return } else if !errors.Is(err, os.ErrNotExist) { t.Fatalf("Stat(%q) error = %v", path, err) } time.Sleep(10 * time.Millisecond) } t.Fatalf("helper file %q was not created", path) }