Add piped child process support
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -117,6 +119,146 @@ func TestRunReturnsCancellation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedExchangesBeforeInputEOFAndPreservesSettings(t *testing.T) {
|
||||
workdir := filepath.Join(t.TempDir(), "piped directory")
|
||||
if err := os.Mkdir(workdir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("RELEASEKIT_PROCESS_OVERLAY", "parent")
|
||||
t.Setenv("RELEASEKIT_PROCESS_INHERITED", "inherited")
|
||||
var stderr bytes.Buffer
|
||||
child, err := StartPiped(context.Background(), Invocation{
|
||||
Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "argument with spaces", "exchange"},
|
||||
Dir: workdir,
|
||||
Env: map[string]string{
|
||||
"RELEASEKIT_PROCESS_HELPER": "1",
|
||||
"RELEASEKIT_PROCESS_OVERLAY": "child",
|
||||
},
|
||||
Stderr: &stderr,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(child.Stdin(), "request\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := bufio.NewReader(child.Stdout()).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("read response before closing input: %v", err)
|
||||
}
|
||||
want := fmt.Sprintf("request|%s|argument with spaces|child|inherited\n", workdir)
|
||||
if response != want {
|
||||
t.Fatalf("piped response = %q, want %q", response, want)
|
||||
}
|
||||
if err := child.Stdin().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, child.Stdout()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := child.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := stderr.String(); got != "child stderr\n" {
|
||||
t.Fatalf("child stderr = %q, want %q", got, "child stderr\n")
|
||||
}
|
||||
if got := os.Getenv("RELEASEKIT_PROCESS_OVERLAY"); got != "parent" {
|
||||
t.Fatalf("parent environment changed: got %q, want %q", got, "parent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedCancellationAfterReadinessReapsChild(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
child, err := StartPiped(ctx, Invocation{
|
||||
Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "wait"},
|
||||
Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1"},
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Fatal(err)
|
||||
}
|
||||
line, err := bufio.NewReader(child.Stdout()).ReadString('\n')
|
||||
if err != nil || line != "started\n" {
|
||||
cancel()
|
||||
_ = child.Stdin().Close()
|
||||
_ = child.Stdout().Close()
|
||||
_ = child.Wait()
|
||||
t.Fatalf("readiness = %q, %v; want started signal", line, err)
|
||||
}
|
||||
cancel()
|
||||
if err := child.Stdin().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := child.Stdout().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := child.Wait(); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Wait() error = %v, want context cancellation", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedCancelDoesNotCancelParentContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
child, err := StartPiped(ctx, Invocation{
|
||||
Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "wait"},
|
||||
Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
line, err := bufio.NewReader(child.Stdout()).ReadString('\n')
|
||||
if err != nil || line != "started\n" {
|
||||
child.Cancel()
|
||||
_ = child.Stdin().Close()
|
||||
_ = child.Stdout().Close()
|
||||
_ = child.Wait()
|
||||
t.Fatalf("readiness = %q, %v; want started signal", line, err)
|
||||
}
|
||||
child.Cancel()
|
||||
if err := child.Stdin().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := child.Stdout().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Wait()
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("Wait() error = %v, want interrupted child exit", err)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("child cancellation changed parent context: %v", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedReportsStartFailure(t *testing.T) {
|
||||
child, err := StartPiped(context.Background(), Invocation{Args: []string{"releasekit-command-that-does-not-exist"}})
|
||||
if err == nil || child != nil || !strings.Contains(err.Error(), "releasekit-command-that-does-not-exist") {
|
||||
t.Fatalf("StartPiped() = %#v, %v; want a missing executable error", child, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedPreservesNonzeroExit(t *testing.T) {
|
||||
child, err := StartPiped(context.Background(), Invocation{
|
||||
Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "exit"},
|
||||
Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := child.Stdin().Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, child.Stdout()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = child.Wait()
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != 7 {
|
||||
t.Fatalf("Wait() error = %v, want exit code 7", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessHelper(t *testing.T) {
|
||||
if os.Getenv("RELEASEKIT_PROCESS_HELPER") != "1" {
|
||||
return
|
||||
@@ -142,6 +284,21 @@ func TestProcessHelper(t *testing.T) {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
case "exchange":
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
request, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
os.Exit(11)
|
||||
}
|
||||
workdir, err := os.Getwd()
|
||||
if err != nil {
|
||||
os.Exit(12)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "%s|%s|%s|%s|%s\n", strings.TrimSuffix(request, "\n"), workdir, args[len(args)-2], os.Getenv("RELEASEKIT_PROCESS_OVERLAY"), os.Getenv("RELEASEKIT_PROCESS_INHERITED"))
|
||||
fmt.Fprintln(os.Stderr, "child stderr")
|
||||
if _, err := io.Copy(io.Discard, reader); err != nil {
|
||||
os.Exit(13)
|
||||
}
|
||||
default:
|
||||
os.Exit(10)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user