Terminate owned subprocess trees
This commit is contained in:
119
internal/adapters/subprocess/process_tree_supported_test.go
Normal file
119
internal/adapters/subprocess/process_tree_supported_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
//go:build linux || darwin || windows
|
||||
|
||||
package subprocess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user