github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/apis/trigger/trigger_test.go (about) 1 package trigger 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "reflect" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/require" 12 apierrors "k8s.io/apimachinery/pkg/api/errors" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/apimachinery/pkg/runtime/schema" 15 "k8s.io/apimachinery/pkg/types" 16 ctrl "sigs.k8s.io/controller-runtime" 17 "sigs.k8s.io/controller-runtime/pkg/builder" 18 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 19 "sigs.k8s.io/controller-runtime/pkg/reconcile" 20 21 "github.com/tilt-dev/tilt/internal/controllers/apiset" 22 "github.com/tilt-dev/tilt/internal/controllers/fake" 23 "github.com/tilt-dev/tilt/internal/controllers/indexer" 24 "github.com/tilt-dev/tilt/internal/timecmp" 25 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 26 ) 27 28 func TestSetupControllerRestartOn(t *testing.T) { 29 cfb := fake.NewControllerFixtureBuilder(t) 30 31 spec := &v1alpha1.RestartOnSpec{ 32 UIButtons: []string{"btn1"}, 33 FileWatches: []string{"fw1"}, 34 } 35 36 c := &fakeReconciler{ 37 indexer: indexer.NewIndexer(cfb.Scheme()), 38 onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) { 39 b.For(&v1alpha1.Cmd{}) 40 SetupControllerRestartOn(b, i, func(_ ctrlclient.Object) *v1alpha1.RestartOnSpec { 41 return spec 42 }) 43 }, 44 } 45 f := cfb.Build(c) 46 47 cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}} 48 f.Create(cmd) 49 c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd) 50 51 ctx := context.Background() 52 reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 53 require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs) 54 55 reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "fw1"}}) 56 require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs) 57 58 // fw named btn1, which doesn't exist 59 reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 60 require.Len(t, reqs, 0) 61 } 62 63 func TestSetupControllerStartOn(t *testing.T) { 64 ctx := context.Background() 65 cfb := fake.NewControllerFixtureBuilder(t) 66 67 spec := &v1alpha1.StartOnSpec{ 68 UIButtons: []string{"btn1"}, 69 } 70 71 c := &fakeReconciler{ 72 indexer: indexer.NewIndexer(cfb.Scheme()), 73 onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) { 74 b.For(&v1alpha1.Cmd{}) 75 SetupControllerStartOn(b, i, func(_ ctrlclient.Object) *v1alpha1.StartOnSpec { 76 return spec 77 }) 78 }, 79 } 80 f := cfb.Build(c) 81 82 cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}} 83 f.Create(cmd) 84 c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd) 85 86 reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 87 require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs) 88 89 // wrong name 90 reqs = c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn2"}}) 91 require.Len(t, reqs, 0) 92 93 // wrong type 94 reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 95 require.Len(t, reqs, 0) 96 } 97 98 func TestSetupControllerStopOn(t *testing.T) { 99 cfb := fake.NewControllerFixtureBuilder(t) 100 101 spec := &v1alpha1.StopOnSpec{ 102 UIButtons: []string{"btn1"}, 103 } 104 105 c := &fakeReconciler{ 106 indexer: indexer.NewIndexer(cfb.Scheme()), 107 onCreateBuilder: func(b *builder.Builder, i *indexer.Indexer) { 108 b.For(&v1alpha1.Cmd{}) 109 SetupControllerStopOn(b, i, func(_ ctrlclient.Object) *v1alpha1.StopOnSpec { 110 return spec 111 }) 112 }, 113 } 114 f := cfb.Build(c) 115 116 cmd := &v1alpha1.Cmd{ObjectMeta: metav1.ObjectMeta{Name: "cmd1"}} 117 f.Create(cmd) 118 c.indexer.OnReconcile(types.NamespacedName{Name: cmd.Name}, cmd) 119 120 ctx := context.Background() 121 reqs := c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 122 require.Equal(t, []reconcile.Request{{NamespacedName: types.NamespacedName{Name: "cmd1"}}}, reqs) 123 124 // wrong name 125 reqs = c.indexer.Enqueue(ctx, &v1alpha1.UIButton{ObjectMeta: metav1.ObjectMeta{Name: "btn2"}}) 126 require.Len(t, reqs, 0) 127 128 // wrong type 129 reqs = c.indexer.Enqueue(ctx, &v1alpha1.FileWatch{ObjectMeta: metav1.ObjectMeta{Name: "btn1"}}) 130 require.Len(t, reqs, 0) 131 } 132 133 func TestLastRestartEvent(t *testing.T) { 134 ctx := context.Background() 135 136 objs := make(apiset.ObjectSet) 137 btns := []*v1alpha1.UIButton{ 138 button("btn10", time.Unix(10, 0)), 139 button("btn0", time.Unix(0, 0)), 140 button("btn3", time.Unix(3, 0)), 141 } 142 for _, btn := range btns { 143 objs.Add(btn) 144 } 145 146 fws := []*v1alpha1.FileWatch{ 147 filewatch("fw2", time.Unix(2, 0)), 148 filewatch("fw20", time.Unix(20, 0)), 149 filewatch("fw7", time.Unix(7, 0)), 150 } 151 for _, fw := range fws { 152 objs.Add(fw) 153 } 154 155 r := &fakeReader{objs: objs} 156 157 for _, tc := range []struct { 158 name string 159 fws []string 160 buttons []string 161 expectedButton string 162 expectedFws []string 163 expectedTime time.Time 164 }{ 165 {"no match", nil, nil, "", nil, time.Time{}}, 166 {"one fw", []string{"fw2"}, nil, "", []string{"fw2"}, time.Unix(2, 0)}, 167 {"one button", nil, []string{"btn3"}, "btn3", nil, time.Unix(3, 0)}, 168 {"all buttons", nil, []string{"btn10", "btn0", "btn3"}, "btn10", nil, time.Unix(10, 0)}, 169 {"all objects", []string{"fw2", "fw20", "fw7"}, []string{"btn10", "btn0", "btn3"}, "", []string{"fw2", "fw20", "fw7"}, time.Unix(20, 0)}, 170 } { 171 t.Run(tc.name, func(t *testing.T) { 172 173 spec := &v1alpha1.RestartOnSpec{ 174 FileWatches: tc.fws, 175 UIButtons: tc.buttons, 176 } 177 178 ts, btn, fws, err := LastRestartEvent(ctx, r, spec) 179 timecmp.RequireTimeEqual(t, tc.expectedTime, ts) 180 buttonName := "" 181 if btn != nil { 182 buttonName = btn.Name 183 } 184 require.Equal(t, tc.expectedButton, buttonName, "button") 185 var fwNames []string 186 for _, fw := range fws { 187 fwNames = append(fwNames, fw.Name) 188 } 189 require.ElementsMatch(t, tc.expectedFws, fwNames, "fws") 190 require.NoError(t, err, "err") 191 }) 192 } 193 } 194 195 func TestLastStartEvent(t *testing.T) { 196 ctx := context.Background() 197 198 objs := make(apiset.ObjectSet) 199 btns := []*v1alpha1.UIButton{ 200 button("btn10", time.Unix(10, 0)), 201 button("btn0", time.Unix(0, 0)), 202 button("btn3", time.Unix(3, 0)), 203 } 204 for _, btn := range btns { 205 objs.Add(btn) 206 } 207 208 r := &fakeReader{objs: objs} 209 210 for _, tc := range []struct { 211 name string 212 buttons []string 213 expectedButton string 214 expectedTime time.Time 215 }{ 216 {"no match", nil, "", time.Time{}}, 217 {"one button", []string{"btn3"}, "btn3", time.Unix(3, 0)}, 218 {"all buttons", []string{"btn10", "btn0", "btn3"}, "btn10", time.Unix(10, 0)}, 219 } { 220 t.Run(tc.name, func(t *testing.T) { 221 222 spec := &v1alpha1.StartOnSpec{ 223 UIButtons: tc.buttons, 224 } 225 226 ts, btn, err := LastStartEvent(ctx, r, spec) 227 require.Equal(t, tc.expectedTime.UTC(), ts.UTC(), "timestamp") 228 buttonName := "" 229 if btn != nil { 230 buttonName = btn.Name 231 } 232 require.Equal(t, tc.expectedButton, buttonName, "button") 233 require.NoError(t, err, "err") 234 }) 235 } 236 } 237 238 func TestLastStopEvent(t *testing.T) { 239 ctx := context.Background() 240 241 objs := make(apiset.ObjectSet) 242 btns := []*v1alpha1.UIButton{ 243 button("btn10", time.Unix(10, 0)), 244 button("btn0", time.Unix(0, 0)), 245 button("btn3", time.Unix(3, 0)), 246 } 247 for _, btn := range btns { 248 objs.Add(btn) 249 } 250 251 r := &fakeReader{objs: objs} 252 253 for _, tc := range []struct { 254 name string 255 buttons []string 256 expectedButton string 257 expectedTime time.Time 258 }{ 259 {"no match", nil, "", time.Time{}}, 260 {"one button", []string{"btn3"}, "btn3", time.Unix(3, 0)}, 261 {"all buttons", []string{"btn10", "btn0", "btn3"}, "btn10", time.Unix(10, 0)}, 262 } { 263 t.Run(tc.name, func(t *testing.T) { 264 265 spec := &v1alpha1.StopOnSpec{ 266 UIButtons: tc.buttons, 267 } 268 269 ts, btn, err := LastStopEvent(ctx, r, spec) 270 timecmp.RequireTimeEqual(t, tc.expectedTime, ts) 271 buttonName := "" 272 if btn != nil { 273 buttonName = btn.Name 274 } 275 require.Equal(t, tc.expectedButton, buttonName, "button") 276 require.NoError(t, err, "err") 277 }) 278 } 279 } 280 281 func TestLastRestartEventError(t *testing.T) { 282 expected := errors.New("oh no") 283 cli := &explodingReader{err: expected} 284 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 285 defer cancel() 286 ts, btn, fw, err := LastRestartEvent(ctx, cli, &v1alpha1.RestartOnSpec{FileWatches: []string{"foo"}}) 287 require.Equal(t, expected, err) 288 require.Zero(t, ts, "Timestamp was not zero value") 289 require.Nil(t, btn, "Button was not nil") 290 require.Nil(t, fw, "FileWatch was not nil") 291 } 292 293 func TestLastStartEventError(t *testing.T) { 294 expected := errors.New("oh no") 295 cli := &explodingReader{err: expected} 296 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 297 defer cancel() 298 ts, btn, err := LastStartEvent(ctx, cli, &v1alpha1.StartOnSpec{UIButtons: []string{"foo"}}) 299 require.Equal(t, expected, err) 300 require.Zero(t, ts, "Timestamp was not zero value") 301 require.Nil(t, btn, "Button was not nil") 302 } 303 304 func TestLastStopEventError(t *testing.T) { 305 expected := errors.New("oh no") 306 cli := &explodingReader{err: expected} 307 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 308 defer cancel() 309 ts, btn, err := LastStopEvent(ctx, cli, &v1alpha1.StopOnSpec{UIButtons: []string{"foo"}}) 310 require.Equal(t, expected, err) 311 require.Zero(t, ts, "Timestamp was not zero value") 312 require.Nil(t, btn, "Button was not nil") 313 } 314 315 type explodingReader struct { 316 err error 317 } 318 319 func (e explodingReader) Get(_ context.Context, _ ctrlclient.ObjectKey, _ ctrlclient.Object, _ ...ctrlclient.GetOption) error { 320 return e.err 321 } 322 323 func (e explodingReader) List(_ context.Context, _ ctrlclient.ObjectList, _ ...ctrlclient.ListOption) error { 324 return e.err 325 } 326 327 type fakeReader struct { 328 objs apiset.ObjectSet 329 } 330 331 func (f *fakeReader) Get(ctx context.Context, key ctrlclient.ObjectKey, out ctrlclient.Object, _ ...ctrlclient.GetOption) error { 332 if f.objs == nil { 333 return errors.New("fakeReader.objs uninitialized") 334 } 335 336 typedObjectSet := f.objs.GetSetForType(out.(apiset.Object)) 337 obj, ok := typedObjectSet[key.Name] 338 if !ok { 339 return apierrors.NewNotFound(schema.GroupResource{}, key.Name) 340 } 341 342 outVal := reflect.ValueOf(out) 343 objVal := reflect.ValueOf(obj) 344 if !objVal.Type().AssignableTo(outVal.Type()) { 345 return fmt.Errorf("fakeReader objs[%s] is type %s, but %s was asked for", key.Name, objVal.Type(), outVal.Type()) 346 } 347 reflect.Indirect(outVal).Set(reflect.Indirect(objVal)) 348 return nil 349 } 350 351 func (f *fakeReader) List(ctx context.Context, list ctrlclient.ObjectList, opts ...ctrlclient.ListOption) error { 352 panic("implement me") 353 } 354 355 var _ ctrlclient.Reader = &fakeReader{} 356 357 type fakeReconciler struct { 358 indexer *indexer.Indexer 359 onCreateBuilder func(builder *builder.Builder, idxer *indexer.Indexer) 360 } 361 362 func (fr *fakeReconciler) CreateBuilder(mgr ctrl.Manager) (*builder.Builder, error) { 363 b := ctrl.NewControllerManagedBy(mgr) 364 fr.onCreateBuilder(b, fr.indexer) 365 366 return b, nil 367 } 368 369 func (fr *fakeReconciler) Reconcile(_ context.Context, _ reconcile.Request) (reconcile.Result, error) { 370 return reconcile.Result{}, nil 371 } 372 373 func button(name string, ts time.Time) *v1alpha1.UIButton { 374 return &v1alpha1.UIButton{ 375 ObjectMeta: metav1.ObjectMeta{Name: name}, 376 Status: v1alpha1.UIButtonStatus{LastClickedAt: metav1.NewMicroTime(ts)}, 377 } 378 } 379 380 func filewatch(name string, ts time.Time) *v1alpha1.FileWatch { 381 return &v1alpha1.FileWatch{ 382 ObjectMeta: metav1.ObjectMeta{Name: name}, 383 Status: v1alpha1.FileWatchStatus{LastEventTime: metav1.NewMicroTime(ts)}, 384 } 385 }