/ internal / memory / supervisor_test.go
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  }