Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions internal/project/waitdelay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import (
// helperEnv selects which half of the process tree a helper run plays.
const helperEnv = "LICH_WAITDELAY_HELPER"

// releaseEnv names the directory the grandchild watches: the test writes
// releaseEnv names the directory the grandchild and the test signal each other
// in: the grandchild writes holdingFile once it holds the pipe, the test writes
// releaseFile there once it is done with the grandchild, which then exits and
// writes exitedFile. Left to sleep out its hold, the grandchild outlived a
// package that finished sooner, still running the test binary go test was
// deleting: "unlinkat ... project.test.exe: Access is denied" on Windows.
const (
releaseEnv = "LICH_WAITDELAY_RELEASE"
holdingFile = "holding"
releaseFile = "release"
exitedFile = "exited"
)
Expand Down Expand Up @@ -50,6 +52,9 @@ func TestSubprocessHelper(t *testing.T) {
os.Exit(0)
case "grandchild":
dir := os.Getenv(releaseEnv)
if err := os.WriteFile(filepath.Join(dir, holdingFile), nil, 0o644); err != nil {
os.Exit(1)
}
awaitFile(filepath.Join(dir, releaseFile), grandchildHold)
if err := os.WriteFile(filepath.Join(dir, exitedFile), nil, 0o644); err != nil {
os.Exit(1)
Expand All @@ -72,6 +77,11 @@ func awaitFile(path string, limit time.Duration) bool {
// here exits immediately and the grandchild behind it keeps stdout open until
// released: with nothing closing that pipe, Output waits for it and reports
// success, deadline or none.
//
// The deadline is the moment the grandchild holds the pipe, not a timer: a
// fixed 200ms ran out on Windows before the test binary had started twice, the
// context killed the child before it spawned the grandchild, and the cleanup
// waited ten seconds for a grandchild that never existed.
func TestSubprocessBudgetOutlivesAGrandchild(t *testing.T) {
t.Parallel()
dir := t.TempDir()
Expand All @@ -83,16 +93,30 @@ func TestSubprocessBudgetOutlivesAGrandchild(t *testing.T) {
t.Fatal("the grandchild did not exit once released")
}
})
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := commandContext(ctx, os.Args[0], "-test.run=TestSubprocessHelper")
cmd.Env = append(cmd.Env, helperEnv+"=child", releaseEnv+"="+dir)

start := time.Now()
type deadline struct {
held bool
at time.Time
}
reached := make(chan deadline, 1)
go func() {
held := awaitFile(filepath.Join(dir, holdingFile), grandchildHold)
cancel()
reached <- deadline{held: held, at: time.Now()}
}()

_, err := cmd.Output()
elapsed := time.Since(start)
returned := time.Now()
d := <-reached

if elapsed > 5*time.Second {
t.Fatalf("Output blocked %s on a grandchild holding the pipe, err %v: the 200ms budget bounded the child alone", elapsed, err)
if !d.held {
t.Fatalf("the grandchild never held the pipe within %s, Output err %v", grandchildHold, err)
}
if elapsed := returned.Sub(d.at); elapsed > 5*time.Second {
t.Fatalf("Output blocked %s past the deadline on a grandchild holding the pipe, err %v: the budget bounded the child alone", elapsed, err)
}
}
Loading