package host import ( "context" "testing" "time" ) // The probe owners' captured env pins live as long as their wake (home part // 2, Task 2b fix round 5). Select captures a pin (in Prerequisites, and // again in Admit); every start attempt of the wake materializes under the // activator start loop's context, which it cancels when the wake ends: // completed, terminally failed or abandoned. A retry of the same launch // finds the pin; the end of the wake drops it; a wake that never reaches // Materialize leaves nothing past its deadline. func (f *poFixture) hasEnvPin() bool { f.owners.mu.Lock() defer f.owners.mu.Unlock() _, ok := f.owners.envPins[poHost] return ok } // envPinGone waits for the wake's end to drop the pin (context.AfterFunc // runs in its own goroutine). func (f *poFixture) envPinGone(t *testing.T, why string) { t.Helper() for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { if !f.hasEnvPin() { return } } t.Fatalf("%s: the captured pin outlived its wake", why) } func TestCapturedEnvPinLivesWithItsWake(t *testing.T) { t.Run("a completed wake, with a retry", func(t *testing.T) { f := newPOFixture(t) o := f.read() in, err := f.owners.Select(t.Context(), f.route, o, poProbe) if err != nil { t.Fatal(err) } admitted := f.admit(o, in) wake, end := context.WithCancel(t.Context()) for attempt := 0; attempt < 2; attempt++ { // the second is a retry of the same launch: it finds the pin if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err != nil { t.Fatalf("attempt %d: %v", attempt, err) } if !f.hasEnvPin() { t.Fatalf("attempt %d: the pin is gone while its wake runs", attempt) } } end() // the start loop returns: the wake completed f.envPinGone(t, "a completed wake") }) t.Run("a failed wake", func(t *testing.T) { f := newPOFixture(t) o := f.read() in, err := f.owners.Select(t.Context(), f.route, o, poProbe) if err != nil { t.Fatal(err) } admitted := f.admit(o, in) // The held render no longer matches the pins: every attempt fails. f.renderOverlay = "changed" wake, end := context.WithCancel(t.Context()) for attempt := 0; attempt < 2; attempt++ { if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err == nil { t.Fatalf("attempt %d materialized changed inputs", attempt) } if !f.hasEnvPin() { t.Fatalf("attempt %d: the pin is gone before the wake ends", attempt) } } end() // the start loop gives up: the wake failed f.envPinGone(t, "a failed wake") }) t.Run("a wake abandoned before Materialize", func(t *testing.T) { f := newPOFixture(t) now := f.owners.Now() policy := poProbe policy.Deadline = now.Add(time.Minute) if _, err := f.owners.Select(t.Context(), f.route, f.read(), policy); err != nil { t.Fatal(err) } if !f.hasEnvPin() { t.Fatal("Select captured no pin") } f.owners.Now = func() time.Time { return now.Add(time.Minute) } if _, ok := f.owners.capturedPin(poHost, nil); ok || f.hasEnvPin() { t.Fatal("a pin past its wake's deadline is still kept") } }) t.Run("an earlier wake's end keeps a later capture", func(t *testing.T) { f := newPOFixture(t) o := f.read() in, err := f.owners.Select(t.Context(), f.route, o, poProbe) if err != nil { t.Fatal(err) } admitted := f.admit(o, in) wake, end := context.WithCancel(t.Context()) if err := f.owners.Materialize(wake, admitted, in, poWork{action: "spawn", f: f}); err != nil { t.Fatal(err) } // a later wake of the same host captures again before the first // one's end is observed if _, err := f.owners.Select(t.Context(), f.route, admitted, poProbe); err != nil { t.Fatal(err) } end() time.Sleep(50 * time.Millisecond) if !f.hasEnvPin() { t.Fatal("the end of an earlier wake dropped a later capture") } }) }