github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/subscriber_test.go (about) 1 package store 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "testing" 8 "time" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 "k8s.io/apimachinery/pkg/types" 13 14 "github.com/tilt-dev/tilt/pkg/logger" 15 ) 16 17 func newCtx() context.Context { 18 return logger.WithLogger(context.Background(), logger.NewTestLogger(os.Stderr)) 19 } 20 21 func TestSubscriber(t *testing.T) { 22 st, _ := NewStoreWithFakeReducer() 23 ctx := newCtx() 24 s := newFakeSubscriber() 25 require.NoError(t, st.AddSubscriber(ctx, s)) 26 27 st.NotifySubscribers(ctx, ChangeSummary{Legacy: true}) 28 call := <-s.onChange 29 close(call.done) 30 assert.True(t, call.summary.Legacy) 31 } 32 33 func TestSubscriberInterleavedCalls(t *testing.T) { 34 st, _ := NewStoreWithFakeReducer() 35 ctx := newCtx() 36 s := newFakeSubscriber() 37 require.NoError(t, st.AddSubscriber(ctx, s)) 38 39 st.NotifySubscribers(ctx, ChangeSummary{}) 40 call := <-s.onChange 41 st.NotifySubscribers(ctx, LegacyChangeSummary()) 42 st.NotifySubscribers(ctx, LegacyChangeSummary()) 43 time.Sleep(10 * time.Millisecond) 44 close(call.done) 45 46 call = <-s.onChange 47 close(call.done) 48 49 select { 50 case <-s.onChange: 51 t.Fatal("Expected no more onChange calls") 52 case <-time.After(10 * time.Millisecond): 53 } 54 } 55 56 func TestSubscriberInterleavedCallsSummary(t *testing.T) { 57 st, _ := NewStoreWithFakeReducer() 58 ctx := newCtx() 59 s := newFakeSubscriber() 60 require.NoError(t, st.AddSubscriber(ctx, s)) 61 62 nn1 := types.NamespacedName{Name: "spec-1"} 63 64 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn1)}) 65 call := <-s.onChange 66 assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1)}) 67 68 nn2 := types.NamespacedName{Name: "spec-2"} 69 nn3 := types.NamespacedName{Name: "spec-3"} 70 71 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn2)}) 72 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn3)}) 73 time.Sleep(10 * time.Millisecond) 74 close(call.done) 75 76 call = <-s.onChange 77 assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn2, nn3)}) 78 close(call.done) 79 80 select { 81 case <-s.onChange: 82 t.Fatal("Expected no more onChange calls") 83 case <-time.After(10 * time.Millisecond): 84 } 85 } 86 87 func TestAddSubscriberToAlreadySetUpListCallsSetUp(t *testing.T) { 88 st, _ := NewStoreWithFakeReducer() 89 ctx := newCtx() 90 _ = st.subscribers.SetUp(ctx, st) 91 92 s := newFakeSubscriber() 93 require.NoError(t, st.AddSubscriber(ctx, s)) 94 95 assert.Equal(t, 1, s.setupCount) 96 } 97 98 func TestAddSubscriberBeforeSetupNoop(t *testing.T) { 99 st, _ := NewStoreWithFakeReducer() 100 ctx := newCtx() 101 102 s := newFakeSubscriber() 103 require.NoError(t, st.AddSubscriber(ctx, s)) 104 105 // We haven't called SetUp on subscriber list as a whole, so won't call it on a new individual subscriber 106 assert.Equal(t, 0, s.setupCount) 107 } 108 109 func TestRemoveSubscriber(t *testing.T) { 110 st, _ := NewStoreWithFakeReducer() 111 ctx := newCtx() 112 s := newFakeSubscriber() 113 114 require.NoError(t, st.AddSubscriber(ctx, s)) 115 st.NotifySubscribers(ctx, ChangeSummary{}) 116 s.assertOnChangeCount(t, 1) 117 118 err := st.RemoveSubscriber(ctx, s) 119 assert.NoError(t, err) 120 st.NotifySubscribers(ctx, LegacyChangeSummary()) 121 s.assertOnChangeCount(t, 0) 122 } 123 124 func TestRemoveSubscriberNotFound(t *testing.T) { 125 st, _ := NewStoreWithFakeReducer() 126 s := newFakeSubscriber() 127 ctx := newCtx() 128 err := st.RemoveSubscriber(ctx, s) 129 if assert.Error(t, err) { 130 assert.Contains(t, err.Error(), "Subscriber not found") 131 } 132 } 133 134 func TestSubscriberSetup(t *testing.T) { 135 st, _ := NewStoreWithFakeReducer() 136 ctx := newCtx() 137 s := newFakeSubscriber() 138 require.NoError(t, st.AddSubscriber(ctx, s)) 139 140 _ = st.subscribers.SetUp(ctx, st) 141 142 assert.Equal(t, 1, s.setupCount) 143 } 144 145 func TestSubscriberTeardown(t *testing.T) { 146 st, _ := NewStoreWithFakeReducer() 147 ctx := newCtx() 148 s := newFakeSubscriber() 149 require.NoError(t, st.AddSubscriber(ctx, s)) 150 151 go st.Dispatch(NewErrorAction(context.Canceled)) 152 err := st.Loop(ctx) 153 if assert.Error(t, err) { 154 assert.Contains(t, err.Error(), "context canceled") 155 } 156 157 assert.Equal(t, 1, s.teardownCount) 158 } 159 160 func TestSubscriberTeardownOnRemove(t *testing.T) { 161 st, _ := NewStoreWithFakeReducer() 162 ctx := newCtx() 163 s := newFakeSubscriber() 164 require.NoError(t, st.AddSubscriber(ctx, s)) 165 166 errChan := make(chan error) 167 go func() { 168 err := st.Loop(ctx) 169 errChan <- err 170 }() 171 172 // Make sure the loop has started. 173 st.NotifySubscribers(ctx, ChangeSummary{}) 174 s.assertOnChangeCount(t, 1) 175 176 // Remove the subscriber and make sure it doesn't get a change. 177 _ = st.RemoveSubscriber(ctx, s) 178 st.NotifySubscribers(ctx, ChangeSummary{}) 179 s.assertOnChangeCount(t, 0) 180 181 assert.Equal(t, 1, s.teardownCount) 182 183 st.Dispatch(NewErrorAction(context.Canceled)) 184 185 err := <-errChan 186 if assert.Error(t, err) { 187 assert.Contains(t, err.Error(), "context canceled") 188 } 189 assert.Equal(t, 1, s.teardownCount) 190 } 191 192 type blockingSleeper struct { 193 SleepDur chan time.Duration 194 } 195 196 func newBlockingSleeper() blockingSleeper { 197 return blockingSleeper{SleepDur: make(chan time.Duration)} 198 } 199 200 func (s blockingSleeper) Sleep(ctx context.Context, d time.Duration) { 201 if d == actionBatchWindow { 202 return 203 } 204 s.SleepDur <- d 205 } 206 207 func TestSubscriberBackoff(t *testing.T) { 208 bs := newBlockingSleeper() 209 st, _ := NewStoreWithFakeReducer() 210 st.sleeper = bs 211 212 ctx := newCtx() 213 s := newFakeSubscriber() 214 require.NoError(t, st.AddSubscriber(ctx, s)) 215 216 // Fire a change that fails 217 nn1 := types.NamespacedName{Name: "spec-1"} 218 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn1)}) 219 220 call := <-s.onChange 221 assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1)}) 222 call.done <- fmt.Errorf("failed") 223 224 // Fire a second change while the first change is sleeping 225 nn2 := types.NamespacedName{Name: "spec-2"} 226 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn2)}) 227 time.Sleep(10 * time.Millisecond) 228 229 // Clear the sleeper, and process the retry. 230 assert.Equal(t, time.Second, <-bs.SleepDur) 231 232 call = <-s.onChange 233 assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1, nn2), LastBackoff: time.Second}) 234 call.done <- fmt.Errorf("failed") 235 236 // Fire a third change while the retry is sleeping 237 nn3 := types.NamespacedName{Name: "spec-3"} 238 st.NotifySubscribers(ctx, ChangeSummary{CmdSpecs: NewChangeSet(nn3)}) 239 time.Sleep(10 * time.Millisecond) 240 241 // Clear the sleeper, and process the second retry. 242 assert.Equal(t, 2*time.Second, <-bs.SleepDur) 243 244 call = <-s.onChange 245 assert.Equal(t, call.summary, ChangeSummary{CmdSpecs: NewChangeSet(nn1, nn2, nn3), LastBackoff: 2 * time.Second}) 246 close(call.done) 247 } 248 249 type subscriberWithPointerReceiver struct { 250 } 251 252 func (s *subscriberWithPointerReceiver) OnChange(ctx context.Context, st RStore, summary ChangeSummary) error { 253 return nil 254 } 255 256 type subscriberWithNonPointerReceiver struct { 257 } 258 259 func (s subscriberWithNonPointerReceiver) OnChange(ctx context.Context, st RStore, summary ChangeSummary) error { 260 return nil 261 } 262 263 func TestSubscriberName(t *testing.T) { 264 require.Equal(t, "store.subscriberWithPointerReceiver", subscriberName(&subscriberWithPointerReceiver{})) 265 require.Equal(t, "store.subscriberWithNonPointerReceiver", subscriberName(subscriberWithNonPointerReceiver{})) 266 }