package process import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) func TestRunPassesArgumentsDirectoryAndEnvironment(t *testing.T) { workdir := filepath.Join(t.TempDir(), "directory with spaces") if err := os.Mkdir(workdir, 0o700); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_PROCESS_OVERLAY", "parent") t.Setenv("RELEASEKIT_PROCESS_INHERITED", "inherited") var stdout, stderr bytes.Buffer err := Run(context.Background(), Invocation{ Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "argument with spaces", "success"}, Dir: workdir, Env: map[string]string{ "RELEASEKIT_PROCESS_HELPER": "1", "RELEASEKIT_PROCESS_OVERLAY": "child", }, Stdout: &stdout, Stderr: &stderr, }) if err != nil { t.Fatal(err) } want := fmt.Sprintf("%s|argument with spaces|child|inherited", workdir) if stdout.String() != want { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } if stderr.String() != "child stderr\n" { t.Fatalf("stderr = %q, want %q", stderr.String(), "child stderr\n") } if got := os.Getenv("RELEASEKIT_PROCESS_OVERLAY"); got != "parent" { t.Fatalf("parent environment changed: got %q, want %q", got, "parent") } } func TestRunPreservesExitError(t *testing.T) { err := Run(context.Background(), Invocation{ Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "exit"}, Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1"}, }) var exitErr *exec.ExitError if !errors.As(err, &exitErr) || exitErr.ExitCode() != 7 { t.Fatalf("Run() error = %v, want exit code 7", err) } } func TestRunReportsMissingExecutable(t *testing.T) { err := Run(context.Background(), Invocation{Args: []string{"releasekit-command-that-does-not-exist"}}) if err == nil || !strings.Contains(err.Error(), "releasekit-command-that-does-not-exist") { t.Fatalf("Run() error = %v, want missing executable diagnostic", err) } } func TestRunReturnsCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() started := make(chan struct{}, 1) invocation := Invocation{ Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "wait"}, Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1"}, Stdout: writerFunc(func([]byte) (int, error) { select { case started <- struct{}{}: default: } return len("started\n"), nil }), } result := make(chan error, 1) go func() { result <- Run(ctx, invocation) }() select { case <-started: cancel() case <-time.After(5 * time.Second): t.Fatal("child did not start") } select { case err := <-result: if !errors.Is(err, context.Canceled) { t.Fatalf("Run() error = %v, want context cancellation", err) } case <-time.After(5 * time.Second): t.Fatal("Run() did not return after cancellation") } } func TestProcessHelper(t *testing.T) { if os.Getenv("RELEASEKIT_PROCESS_HELPER") != "1" { return } args := os.Args if len(args) < 2 { os.Exit(8) } mode := args[len(args)-1] switch mode { case "success": workdir, err := os.Getwd() if err != nil { os.Exit(9) } fmt.Fprintf(os.Stdout, "%s|%s|%s|%s", workdir, args[len(args)-2], os.Getenv("RELEASEKIT_PROCESS_OVERLAY"), os.Getenv("RELEASEKIT_PROCESS_INHERITED")) fmt.Fprintln(os.Stderr, "child stderr") os.Exit(0) case "exit": os.Exit(7) case "wait": fmt.Fprintln(os.Stdout, "started") for { time.Sleep(time.Second) } default: os.Exit(10) } } type writerFunc func([]byte) (int, error) func (write writerFunc) Write(p []byte) (int, error) { return write(p) } func helperExecutable(t *testing.T) string { t.Helper() executable, err := os.Executable() if err != nil { t.Fatal(err) } return executable }