supervisor_test.go
1 package memory 2 3 import ( 4 "context" 5 "errors" 6 "sync/atomic" 7 "testing" 8 "time" 9 ) 10 11 // fakeSpawner records spawn count and lets the test control whether each 12 // spawn cycle reports failure or success at the WaitReady step. 13 type fakeSpawner struct { 14 failsRemaining atomic.Int32 // first N Spawn or WaitReady calls fail 15 spawned atomic.Int32 16 waitReadyErr error 17 waitErr error 18 onReadyHit atomic.Int32 // observed via the Supervisor's onReady callback 19 } 20 21 func (f *fakeSpawner) Spawn(ctx context.Context) error { 22 f.spawned.Add(1) 23 if f.failsRemaining.Load() > 0 { 24 f.failsRemaining.Add(-1) 25 return errors.New("simulated spawn failure") 26 } 27 return nil 28 } 29 30 func (f *fakeSpawner) WaitReady(ctx context.Context, _ time.Duration) error { 31 return f.waitReadyErr 32 } 33 34 func (f *fakeSpawner) Wait() error { 35 return f.waitErr 36 } 37 38 func TestSupervisor_BackoffAndDegradedAfterBudget(t *testing.T) { 39 sp := &fakeSpawner{} 40 sp.failsRemaining.Store(10) // always fail 41 sup := NewSupervisor(sp, 3, func() { sp.onReadyHit.Add(1) }) 42 sup.testBackoff = func(int) time.Duration { return 1 * time.Millisecond } 43 final := sup.Run(context.Background()) 44 if final != StateDegraded { 45 t.Fatalf("final=%v want Degraded", final) 46 } 47 if got := sp.spawned.Load(); got < 3 { 48 t.Fatalf("spawned=%d want >=3", got) 49 } 50 if sp.onReadyHit.Load() != 0 { 51 t.Fatal("onReady should not fire when sidecar never becomes ready") 52 } 53 } 54 55 func TestSupervisor_RecoversFromColdStartFailure(t *testing.T) { 56 // Spec acceptance #53: first WaitReady failure must be recoverable. 57 sp := &fakeSpawner{} 58 sp.failsRemaining.Store(2) // first 2 Spawns fail; 3rd succeeds 59 sp.waitErr = errors.New("simulated crash") // child exits after becoming ready 60 sup := NewSupervisor(sp, 4, func() { sp.onReadyHit.Add(1) }) 61 sup.testBackoff = func(int) time.Duration { return 1 * time.Millisecond } 62 final := sup.Run(context.Background()) 63 if sp.onReadyHit.Load() == 0 { 64 t.Fatal("onReady should have fired at least once") 65 } 66 if final != StateDegraded { 67 // Eventually budget should run out because Wait keeps returning err. 68 t.Fatalf("final=%v want Degraded", final) 69 } 70 } 71 72 func TestSupervisor_CtxCancelExitsCleanly(t *testing.T) { 73 sp := &fakeSpawner{} 74 sp.failsRemaining.Store(100) 75 sup := NewSupervisor(sp, 100, nil) 76 sup.testBackoff = func(int) time.Duration { return 50 * time.Millisecond } 77 ctx, cancel := context.WithCancel(context.Background()) 78 go func() { time.Sleep(20 * time.Millisecond); cancel() }() 79 final := sup.Run(ctx) 80 if final != StateStopped { 81 t.Fatalf("final=%v want Stopped", final) 82 } 83 }