github.com/tilt-dev/tilt@v0.36.0/internal/engine/buildcontroller_test.go (about) 1 package engine 2 3 import ( 4 "errors" 5 "fmt" 6 "runtime" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/apimachinery/pkg/types" 15 16 "github.com/tilt-dev/tilt/internal/container" 17 "github.com/tilt-dev/tilt/internal/controllers/apis/uibutton" 18 "github.com/tilt-dev/tilt/internal/k8s" 19 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 20 "github.com/tilt-dev/tilt/internal/store" 21 "github.com/tilt-dev/tilt/internal/testutils/configmap" 22 "github.com/tilt-dev/tilt/internal/testutils/manifestbuilder" 23 "github.com/tilt-dev/tilt/internal/testutils/podbuilder" 24 "github.com/tilt-dev/tilt/internal/watch" 25 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 26 "github.com/tilt-dev/tilt/pkg/model" 27 ) 28 29 func TestBuildControllerLocalResource(t *testing.T) { 30 f := newTestFixture(t) 31 32 dep := f.JoinPath("stuff.json") 33 manifest := manifestbuilder.New(f, "local"). 34 WithLocalResource("echo beep boop", []string{dep}). 35 Build() 36 f.Start([]model.Manifest{manifest}) 37 38 call := f.nextCallComplete() 39 lt := manifest.LocalTarget() 40 assert.Equal(t, lt, call.local()) 41 42 f.fsWatcher.Events <- watch.NewFileEvent(dep) 43 44 call = f.nextCallComplete() 45 assert.Equal(t, lt, call.local()) 46 47 f.WaitUntilManifestState("local target manifest state not updated", "local", func(ms store.ManifestState) bool { 48 lrs := ms.RuntimeState.(store.LocalRuntimeState) 49 return !lrs.LastReadyOrSucceededTime.IsZero() && lrs.RuntimeStatus() == v1alpha1.RuntimeStatusNotApplicable 50 }) 51 52 err := f.Stop() 53 assert.NoError(t, err) 54 f.assertAllBuildsConsumed() 55 } 56 57 func TestBuildControllerManualTriggerBuildReasonInit(t *testing.T) { 58 for _, tc := range []struct { 59 name string 60 triggerMode model.TriggerMode 61 }{ 62 {"fully manual", model.TriggerModeManual}, 63 {"manual with auto init", model.TriggerModeManualWithAutoInit}, 64 } { 65 t.Run(tc.name, func(t *testing.T) { 66 f := newTestFixture(t) 67 mName := model.ManifestName("foobar") 68 manifest := f.newManifest(mName.String()).WithTriggerMode(tc.triggerMode) 69 manifests := []model.Manifest{manifest} 70 f.Start(manifests) 71 72 // make sure there's a first build 73 if !manifest.TriggerMode.AutoInitial() { 74 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: mName}) 75 } 76 77 f.nextCallComplete() 78 79 f.withManifestState(mName, func(ms store.ManifestState) { 80 require.Equal(t, tc.triggerMode.AutoInitial(), ms.LastBuild().Reason.Has(model.BuildReasonFlagInit)) 81 }) 82 }) 83 } 84 } 85 86 func TestTriggerModes(t *testing.T) { 87 for _, tc := range []struct { 88 name string 89 triggerMode model.TriggerMode 90 expectInitialBuild bool 91 expectBuildWhenFilesChange bool 92 }{ 93 {name: "fully auto", triggerMode: model.TriggerModeAuto, expectInitialBuild: true, expectBuildWhenFilesChange: true}, 94 {name: "auto with manual init", triggerMode: model.TriggerModeAutoWithManualInit, expectInitialBuild: false, expectBuildWhenFilesChange: true}, 95 {name: "manual with auto init", triggerMode: model.TriggerModeManualWithAutoInit, expectInitialBuild: true, expectBuildWhenFilesChange: false}, 96 {name: "fully manual", triggerMode: model.TriggerModeManual, expectInitialBuild: false, expectBuildWhenFilesChange: false}, 97 } { 98 t.Run(tc.name, func(t *testing.T) { 99 f := newTestFixture(t) 100 101 manifest := f.simpleManifestWithTriggerMode("foobar", tc.triggerMode) 102 manifests := []model.Manifest{manifest} 103 f.Start(manifests) 104 105 // basic check of trigger mode properties 106 assert.Equal(t, tc.expectInitialBuild, tc.triggerMode.AutoInitial()) 107 assert.Equal(t, tc.expectBuildWhenFilesChange, tc.triggerMode.AutoOnChange()) 108 109 // if we expect an initial build from the manifest, wait for it to complete 110 if tc.expectInitialBuild { 111 f.nextCallComplete("initial build") 112 } 113 114 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("main.go")) 115 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 116 return st.BuildStatus(manifest.ImageTargetAt(0).ID()).CountPendingFileChanges() >= 1 117 }) 118 119 if !tc.expectBuildWhenFilesChange { 120 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 121 return 122 } 123 124 call := f.nextCallComplete("build after file change") 125 state := call.oneImageState() 126 assert.Equal(t, []string{f.JoinPath("main.go")}, state.FilesChanged()) 127 }) 128 } 129 } 130 131 func TestBuildControllerImageBuildTrigger(t *testing.T) { 132 for _, tc := range []struct { 133 name string 134 triggerMode model.TriggerMode 135 filesChanged bool 136 expectedImageBuild bool 137 }{ 138 {name: "fully manual with change", triggerMode: model.TriggerModeManual, filesChanged: true, expectedImageBuild: false}, 139 {name: "manual with auto init with change", triggerMode: model.TriggerModeManualWithAutoInit, filesChanged: true, expectedImageBuild: false}, 140 {name: "fully manual without change", triggerMode: model.TriggerModeManual, filesChanged: false, expectedImageBuild: true}, 141 {name: "manual with auto init without change", triggerMode: model.TriggerModeManualWithAutoInit, filesChanged: false, expectedImageBuild: true}, 142 {name: "fully auto without change", triggerMode: model.TriggerModeAuto, filesChanged: false, expectedImageBuild: true}, 143 {name: "auto with manual init without change", triggerMode: model.TriggerModeAutoWithManualInit, filesChanged: false, expectedImageBuild: true}, 144 } { 145 t.Run(tc.name, func(t *testing.T) { 146 f := newTestFixture(t) 147 mName := model.ManifestName("foobar") 148 149 manifest := f.simpleManifestWithTriggerMode(mName, tc.triggerMode) 150 manifests := []model.Manifest{manifest} 151 f.Start(manifests) 152 153 // if we expect an initial build from the manifest, wait for it to complete 154 if manifest.TriggerMode.AutoInitial() { 155 f.nextCallComplete() 156 } 157 158 expectedFiles := []string{} 159 if tc.filesChanged { 160 expectedFiles = append(expectedFiles, f.JoinPath("main.go")) 161 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("main.go")) 162 } 163 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 164 return st.BuildStatus(manifest.ImageTargetAt(0).ID()).CountPendingFileChanges() >= len(expectedFiles) 165 }) 166 167 if manifest.TriggerMode.AutoOnChange() { 168 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 169 } 170 171 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: mName}) 172 call := f.nextCallComplete() 173 state := call.oneImageState() 174 assert.Equal(t, expectedFiles, state.FilesChanged()) 175 assert.Equal(t, tc.expectedImageBuild, state.FullBuildTriggered) 176 177 f.WaitUntil("manifest removed from queue", func(st store.EngineState) bool { 178 for _, mn := range st.TriggerQueue { 179 if mn == mName { 180 return false 181 } 182 } 183 return true 184 }) 185 }) 186 } 187 } 188 189 func TestBuildQueueOrdering(t *testing.T) { 190 f := newTestFixture(t) 191 192 m1 := f.newManifestWithRef("manifest1", container.MustParseNamed("manifest1")). 193 WithTriggerMode(model.TriggerModeManualWithAutoInit) 194 m2 := f.newManifestWithRef("manifest2", container.MustParseNamed("manifest2")). 195 WithTriggerMode(model.TriggerModeManualWithAutoInit) 196 m3 := f.newManifestWithRef("manifest3", container.MustParseNamed("manifest3")). 197 WithTriggerMode(model.TriggerModeManual) 198 m4 := f.newManifestWithRef("manifest4", container.MustParseNamed("manifest4")). 199 WithTriggerMode(model.TriggerModeManual) 200 201 // attach to state in different order than we plan to trigger them 202 manifests := []model.Manifest{m4, m2, m3, m1} 203 f.Start(manifests) 204 205 expectedInitialBuildCount := 0 206 for _, m := range manifests { 207 if m.TriggerMode.AutoInitial() { 208 expectedInitialBuildCount++ 209 f.nextCall() 210 } 211 } 212 213 f.waitForCompletedBuildCount(expectedInitialBuildCount) 214 215 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("main.go")) 216 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 217 return st.BuildStatus(m1.ImageTargetAt(0).ID()).HasPendingFileChanges() && 218 st.BuildStatus(m2.ImageTargetAt(0).ID()).HasPendingFileChanges() && 219 st.BuildStatus(m3.ImageTargetAt(0).ID()).HasPendingFileChanges() && 220 st.BuildStatus(m4.ImageTargetAt(0).ID()).HasPendingFileChanges() 221 }) 222 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 223 224 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest1"}) 225 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest2"}) 226 time.Sleep(10 * time.Millisecond) 227 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest3"}) 228 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest4"}) 229 230 for i := range manifests { 231 expName := fmt.Sprintf("manifest%d", i+1) 232 call := f.nextCall() 233 imgID := call.firstImgTarg().ID().String() 234 if assert.True(t, strings.HasSuffix(imgID, expName), 235 "expected to get manifest '%s' but instead got: '%s' (checking suffix for manifest name)", expName, imgID) { 236 assert.Equal(t, []string{f.JoinPath("main.go")}, call.oneImageState().FilesChanged(), 237 "for manifest '%s", expName) 238 } 239 } 240 f.waitForCompletedBuildCount(expectedInitialBuildCount + len(manifests)) 241 } 242 243 func TestBuildQueueAndAutobuildOrdering(t *testing.T) { 244 f := newTestFixture(t) 245 246 // changes to this dir. will register with our manual manifests 247 dirManual := f.JoinPath("dirManual/") 248 // changes to this dir. will register with our automatic manifests 249 dirAuto := f.JoinPath("dirAuto/") 250 251 m1 := f.newDockerBuildManifestWithBuildPath("manifest1", dirManual).WithTriggerMode(model.TriggerModeManualWithAutoInit) 252 m2 := f.newDockerBuildManifestWithBuildPath("manifest2", dirManual).WithTriggerMode(model.TriggerModeManualWithAutoInit) 253 m3 := f.newDockerBuildManifestWithBuildPath("manifest3", dirManual).WithTriggerMode(model.TriggerModeManual) 254 m4 := f.newDockerBuildManifestWithBuildPath("manifest4", dirManual).WithTriggerMode(model.TriggerModeManual) 255 m5 := f.newDockerBuildManifestWithBuildPath("manifest5", dirAuto).WithTriggerMode(model.TriggerModeAuto) 256 257 // attach to state in different order than we plan to trigger them 258 manifests := []model.Manifest{m5, m4, m2, m3, m1} 259 f.Start(manifests) 260 261 expectedInitialBuildCount := 0 262 for _, m := range manifests { 263 if m.TriggerMode.AutoInitial() { 264 expectedInitialBuildCount++ 265 f.nextCall() 266 } 267 } 268 269 f.waitForCompletedBuildCount(expectedInitialBuildCount) 270 271 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("dirManual/main.go")) 272 f.WaitUntil("pending change appears", func(st store.EngineState) bool { 273 return st.BuildStatus(m1.ImageTargetAt(0).ID()).HasPendingFileChanges() && 274 st.BuildStatus(m2.ImageTargetAt(0).ID()).HasPendingFileChanges() && 275 st.BuildStatus(m3.ImageTargetAt(0).ID()).HasPendingFileChanges() && 276 st.BuildStatus(m4.ImageTargetAt(0).ID()).HasPendingFileChanges() 277 }) 278 f.assertNoCall("even tho there are pending changes, manual manifest shouldn't build w/o explicit trigger") 279 280 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest1"}) 281 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest2"}) 282 // make our one auto-trigger manifest build - should be evaluated LAST, after 283 // all the manual manifests waiting in the queue 284 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("dirAuto/main.go")) 285 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest3"}) 286 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: "manifest4"}) 287 288 for i := range manifests { 289 call := f.nextCall() 290 imgTargID := call.firstImgTarg().ID().String() 291 expectSuffix := fmt.Sprintf("manifest%d", i+1) 292 assert.True(t, strings.HasSuffix(imgTargID, expectSuffix), "expect this call to have image target ...%s (got: %s)", expectSuffix, imgTargID) 293 294 if i < 4 { 295 assert.Equal(t, []string{f.JoinPath("dirManual/main.go")}, call.oneImageState().FilesChanged(), "for manifest %d", i+1) 296 } else { 297 // the automatic manifest 298 assert.Equal(t, []string{f.JoinPath("dirAuto/main.go")}, call.oneImageState().FilesChanged(), "for manifest %d", i+1) 299 } 300 } 301 f.waitForCompletedBuildCount(len(manifests) + expectedInitialBuildCount) 302 } 303 304 // any manifests without image targets should be deployed before any manifests WITH image targets 305 func TestBuildControllerNoBuildManifestsFirst(t *testing.T) { 306 f := newTestFixture(t) 307 308 manifests := make([]model.Manifest, 10) 309 for i := 0; i < 10; i++ { 310 manifests[i] = f.newManifest(fmt.Sprintf("built%d", i+1)) 311 } 312 313 for _, i := range []int{3, 7, 8} { 314 manifests[i] = manifestbuilder.New(f, model.ManifestName(fmt.Sprintf("unbuilt%d", i+1))). 315 WithK8sYAML(SanchoYAML). 316 Build() 317 } 318 f.Start(manifests) 319 320 var observedBuildOrder []string 321 for i := 0; i < len(manifests); i++ { 322 call := f.nextCall() 323 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 324 } 325 326 // throwing a bunch of elements at it to increase confidence we maintain order between built and unbuilt 327 // this might miss bugs since we might just get these elements back in the right order via luck 328 expectedBuildOrder := []string{ 329 "unbuilt4", 330 "unbuilt8", 331 "unbuilt9", 332 "built1", 333 "built2", 334 "built3", 335 "built5", 336 "built6", 337 "built7", 338 "built10", 339 } 340 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 341 } 342 343 func TestBuildControllerUnresourcedYAMLFirst(t *testing.T) { 344 f := newTestFixture(t) 345 346 manifests := []model.Manifest{ 347 f.newManifest("built1"), 348 f.newManifest("built2"), 349 f.newManifest("built3"), 350 f.newManifest("built4"), 351 } 352 353 manifests = append(manifests, manifestbuilder.New(f, model.UnresourcedYAMLManifestName). 354 WithK8sYAML(testyaml.SecretYaml).Build()) 355 f.Start(manifests) 356 357 var observedBuildOrder []string 358 for i := 0; i < len(manifests); i++ { 359 call := f.nextCall() 360 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 361 } 362 363 expectedBuildOrder := []string{ 364 model.UnresourcedYAMLManifestName.String(), 365 "built1", 366 "built2", 367 "built3", 368 "built4", 369 } 370 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 371 } 372 373 func TestBuildControllerRespectDockerComposeOrder(t *testing.T) { 374 f := newTestFixture(t) 375 376 sancho := NewSanchoLiveUpdateDCManifest(f) 377 redis := manifestbuilder.New(f, "redis").WithDockerCompose().Build() 378 donQuixote := manifestbuilder.New(f, "don-quixote").WithDockerCompose().Build() 379 manifests := []model.Manifest{redis, sancho, donQuixote} 380 f.Start(manifests) 381 382 var observedBuildOrder []string 383 for i := 0; i < len(manifests); i++ { 384 call := f.nextCall() 385 observedBuildOrder = append(observedBuildOrder, call.dc().Name.String()) 386 } 387 388 // If these were Kubernetes resources, we would try to deploy don-quixote 389 // before sancho, because it doesn't have an image build. 390 // 391 // But this would be wrong, because Docker Compose has stricter ordering requirements, see: 392 // https://docs.docker.com/compose/startup-order/ 393 expectedBuildOrder := []string{ 394 "redis", 395 "sancho", 396 "don-quixote", 397 } 398 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 399 } 400 401 func TestBuildControllerLocalResourcesBeforeClusterResources(t *testing.T) { 402 f := newTestFixture(t) 403 404 manifests := []model.Manifest{ 405 f.newManifest("clusterBuilt1"), 406 f.newManifest("clusterBuilt2"), 407 manifestbuilder.New(f, "clusterUnbuilt"). 408 WithK8sYAML(SanchoYAML).Build(), 409 manifestbuilder.New(f, "local1"). 410 WithLocalResource("echo local1", nil).Build(), 411 f.newManifest("clusterBuilt3"), 412 manifestbuilder.New(f, "local2"). 413 WithLocalResource("echo local2", nil).Build(), 414 } 415 416 manifests = append(manifests, manifestbuilder.New(f, model.UnresourcedYAMLManifestName). 417 WithK8sYAML(testyaml.SecretYaml).Build()) 418 f.Start(manifests) 419 420 var observedBuildOrder []string 421 for i := 0; i < len(manifests); i++ { 422 call := f.nextCall() 423 if !call.k8s().Empty() { 424 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 425 continue 426 } 427 observedBuildOrder = append(observedBuildOrder, call.local().Name.String()) 428 } 429 430 expectedBuildOrder := []string{ 431 "local1", 432 "local2", 433 model.UnresourcedYAMLManifestName.String(), 434 "clusterUnbuilt", 435 "clusterBuilt1", 436 "clusterBuilt2", 437 "clusterBuilt3", 438 } 439 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 440 } 441 442 func TestBuildControllerResourceDeps(t *testing.T) { 443 f := newTestFixture(t) 444 445 depGraph := map[string][]string{ 446 "a": {"e"}, 447 "b": {"e"}, 448 "c": {"d", "g"}, 449 "d": {}, 450 "e": {"d", "f"}, 451 "f": {"c"}, 452 "g": {}, 453 } 454 455 var manifests []model.Manifest 456 podBuilders := make(map[string]podbuilder.PodBuilder) 457 for name, deps := range depGraph { 458 m := f.newManifest(name) 459 for _, dep := range deps { 460 m.ResourceDependencies = append(m.ResourceDependencies, model.ManifestName(dep)) 461 } 462 manifests = append(manifests, m) 463 podBuilders[name] = f.registerForDeployer(m) 464 } 465 466 f.Start(manifests) 467 468 var observedOrder []string 469 for i := range manifests { 470 call := f.nextCall("%dth build. have built: %v", i, observedOrder) 471 name := call.k8s().Name.String() 472 observedOrder = append(observedOrder, name) 473 f.podEvent(podBuilders[name].WithContainerReady(true).Build()) 474 } 475 476 var expectedManifests []string 477 for name := range depGraph { 478 expectedManifests = append(expectedManifests, name) 479 } 480 481 // make sure everything built 482 require.ElementsMatch(t, expectedManifests, observedOrder) 483 484 buildIndexes := make(map[string]int) 485 for i, n := range observedOrder { 486 buildIndexes[n] = i 487 } 488 489 // make sure it happened in an acceptable order 490 for name, deps := range depGraph { 491 for _, dep := range deps { 492 require.Truef(t, buildIndexes[name] > buildIndexes[dep], "%s built before %s, contrary to resource deps", name, dep) 493 } 494 } 495 } 496 497 // normally, local builds go before k8s builds 498 // if the local build depends on the k8s build, the k8s build should go first 499 func TestBuildControllerResourceDepTrumpsLocalResourcePriority(t *testing.T) { 500 f := newTestFixture(t) 501 502 k8sManifest := f.newManifest("foo") 503 pb := f.registerForDeployer(k8sManifest) 504 localManifest := manifestbuilder.New(f, "bar"). 505 WithLocalResource("echo bar", nil). 506 WithResourceDeps("foo").Build() 507 manifests := []model.Manifest{localManifest, k8sManifest} 508 f.Start(manifests) 509 510 var observedBuildOrder []string 511 for i := 0; i < len(manifests); i++ { 512 call := f.nextCall() 513 if !call.k8s().Empty() { 514 observedBuildOrder = append(observedBuildOrder, call.k8s().Name.String()) 515 pb = pb.WithContainerReady(true) 516 f.podEvent(pb.Build()) 517 continue 518 } 519 observedBuildOrder = append(observedBuildOrder, call.local().Name.String()) 520 } 521 522 expectedBuildOrder := []string{"foo", "bar"} 523 assert.Equal(t, expectedBuildOrder, observedBuildOrder) 524 } 525 526 // bar depends on foo, we build foo three times before marking it ready, and make sure bar waits 527 func TestBuildControllerResourceDepTrumpsInitialBuild(t *testing.T) { 528 if runtime.GOOS == "windows" { 529 t.Skip("flaky on windows") 530 } 531 f := newTestFixture(t) 532 533 foo := manifestbuilder.New(f, "foo"). 534 WithLocalResource("foo cmd", []string{f.JoinPath("foo")}). 535 Build() 536 bar := manifestbuilder.New(f, "bar"). 537 WithLocalResource("bar cmd", []string{f.JoinPath("bar")}). 538 WithResourceDeps("foo"). 539 Build() 540 manifests := []model.Manifest{foo, bar} 541 f.SetNextBuildError(errors.New("failure")) 542 f.Start(manifests) 543 544 call := f.nextCall() 545 require.Equal(t, "foo", call.local().Name.String()) 546 547 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 548 f.SetNextBuildError(errors.New("failure")) 549 call = f.nextCall() 550 require.Equal(t, "foo", call.local().Name.String()) 551 552 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 553 call = f.nextCall() 554 require.Equal(t, "foo", call.local().Name.String()) 555 556 // now that the foo build has succeeded, bar should get queued 557 call = f.nextCall() 558 require.Equal(t, "bar", call.local().Name.String()) 559 } 560 561 // bar depends on foo. make sure bar waits on foo even as foo fails 562 func TestBuildControllerResourceDepTrumpsPendingBuild(t *testing.T) { 563 f := newTestFixture(t) 564 565 foo := manifestbuilder.New(f, "foo"). 566 WithLocalResource("foo cmd", []string{f.JoinPath("foo")}). 567 Build() 568 bar := manifestbuilder.New(f, "bar"). 569 WithLocalResource("bar cmd", []string{f.JoinPath("bar")}). 570 WithResourceDeps("foo"). 571 Build() 572 573 manifests := []model.Manifest{bar, foo} 574 f.SetNextBuildError(errors.New("failure")) 575 f.Start(manifests) 576 577 // trigger a change for bar so that it would try to build if not for its resource dep 578 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("bar", "main.go")) 579 580 call := f.nextCall() 581 require.Equal(t, "foo", call.local().Name.String()) 582 583 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("foo", "main.go")) 584 call = f.nextCall() 585 require.Equal(t, "foo", call.local().Name.String()) 586 587 // since the foo build succeeded, bar should now queue 588 call = f.nextCall() 589 require.Equal(t, "bar", call.local().Name.String()) 590 } 591 592 func TestBuildControllerWontBuildManifestIfNoSlotsAvailable(t *testing.T) { 593 f := newTestFixture(t) 594 f.b.completeBuildsManually = true 595 f.setMaxParallelUpdates(2) 596 597 manA := f.newDockerBuildManifestWithBuildPath("manA", f.JoinPath("a")) 598 manB := f.newDockerBuildManifestWithBuildPath("manB", f.JoinPath("b")) 599 manC := f.newDockerBuildManifestWithBuildPath("manC", f.JoinPath("c")) 600 f.Start([]model.Manifest{manA, manB, manC}) 601 f.completeAndCheckBuildsForManifests(manA, manB, manC) 602 603 // start builds for all manifests (we only have 2 build slots) 604 f.editFileAndWaitForManifestBuilding("manA", "a/main.go") 605 f.editFileAndWaitForManifestBuilding("manB", "b/main.go") 606 f.editFileAndAssertManifestNotBuilding("manC", "c/main.go") 607 608 // Complete one build... 609 f.completeBuildForManifest(manA) 610 call := f.nextCall("expect manA build complete") 611 f.assertCallIsForManifestAndFiles(call, manA, "a/main.go") 612 613 // ...and now there's a free build slot for 'manC' 614 f.waitUntilManifestBuilding("manC") 615 616 // complete the rest (can't guarantee order) 617 f.completeAndCheckBuildsForManifests(manB, manC) 618 619 err := f.Stop() 620 assert.NoError(t, err) 621 f.assertAllBuildsConsumed() 622 } 623 624 // It should be legal for a user to change maxParallelUpdates while builds 625 // are in progress (e.g. if there are 5 builds in progress and user sets 626 // maxParallelUpdates=3, nothing should explode.) 627 func TestCurrentlyBuildingMayExceedMaxParallelUpdates(t *testing.T) { 628 f := newTestFixture(t) 629 f.b.completeBuildsManually = true 630 f.setMaxParallelUpdates(3) 631 632 manA := f.newDockerBuildManifestWithBuildPath("manA", f.JoinPath("a")) 633 manB := f.newDockerBuildManifestWithBuildPath("manB", f.JoinPath("b")) 634 manC := f.newDockerBuildManifestWithBuildPath("manC", f.JoinPath("c")) 635 f.Start([]model.Manifest{manA, manB, manC}) 636 f.completeAndCheckBuildsForManifests(manA, manB, manC) 637 638 // start builds for all manifests 639 f.editFileAndWaitForManifestBuilding("manA", "a/main.go") 640 f.editFileAndWaitForManifestBuilding("manB", "b/main.go") 641 f.editFileAndWaitForManifestBuilding("manC", "c/main.go") 642 f.waitUntilNumBuildSlots(0) 643 644 // decrease maxParallelUpdates (now less than the number of current builds, but this is okay) 645 f.setMaxParallelUpdates(2) 646 f.waitUntilNumBuildSlots(0) 647 648 // another file change for manB -- will try to start another build as soon as possible 649 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("b/other.go")) 650 651 f.completeBuildForManifest(manB) 652 call := f.nextCall("expect manB build complete") 653 f.assertCallIsForManifestAndFiles(call, manB, "b/main.go") 654 655 // we should NOT see another build for manB, even though it has a pending file change, 656 // b/c we don't have enough slots (since we decreased maxParallelUpdates) 657 f.waitUntilNumBuildSlots(0) 658 f.waitUntilManifestNotBuilding("manB") 659 660 // complete another build... 661 f.completeBuildForManifest(manA) 662 call = f.nextCall("expect manA build complete") 663 f.assertCallIsForManifestAndFiles(call, manA, "a/main.go") 664 665 // ...now that we have an available slots again, manB will rebuild 666 f.waitUntilManifestBuilding("manB") 667 668 f.completeBuildForManifest(manB) 669 call = f.nextCall("expect manB build complete (second build)") 670 f.assertCallIsForManifestAndFiles(call, manB, "b/other.go") 671 672 f.completeBuildForManifest(manC) 673 call = f.nextCall("expect manC build complete") 674 f.assertCallIsForManifestAndFiles(call, manC, "c/main.go") 675 676 err := f.Stop() 677 assert.NoError(t, err) 678 f.assertAllBuildsConsumed() 679 } 680 681 func TestDontStartBuildIfControllerAndEngineUnsynced(t *testing.T) { 682 f := newTestFixture(t) 683 684 f.b.completeBuildsManually = true 685 f.setMaxParallelUpdates(3) 686 687 manA := f.newDockerBuildManifestWithBuildPath("manA", f.JoinPath("a")) 688 manB := f.newDockerBuildManifestWithBuildPath("manB", f.JoinPath("b")) 689 f.Start([]model.Manifest{manA, manB}) 690 f.completeAndCheckBuildsForManifests(manA, manB) 691 692 f.editFileAndWaitForManifestBuilding("manA", "a/main.go") 693 694 // deliberately de-sync engine state and build controller 695 st := f.store.LockMutableStateForTesting() 696 st.BuildControllerStartCount-- 697 f.store.UnlockMutableState() 698 699 // this build won't start while state and build controller are out of sync 700 f.editFileAndAssertManifestNotBuilding("manB", "b/main.go") 701 702 // resync the two counts... 703 st = f.store.LockMutableStateForTesting() 704 st.BuildControllerStartCount++ 705 f.store.UnlockMutableState() 706 707 // ...and manB build will start as expected 708 f.waitUntilManifestBuilding("manB") 709 710 // complete all builds (can't guarantee order) 711 f.completeAndCheckBuildsForManifests(manA, manB) 712 713 err := f.Stop() 714 assert.NoError(t, err) 715 f.assertAllBuildsConsumed() 716 } 717 718 func TestErrorHandlingWithMultipleBuilds(t *testing.T) { 719 if runtime.GOOS == "windows" { 720 t.Skip("TODO(nick): fix this") 721 } 722 f := newTestFixture(t) 723 f.b.completeBuildsManually = true 724 f.setMaxParallelUpdates(2) 725 726 errA := fmt.Errorf("errA") 727 errB := fmt.Errorf("errB") 728 729 manA := f.newDockerBuildManifestWithBuildPath("manA", f.JoinPath("a")) 730 manB := f.newDockerBuildManifestWithBuildPath("manB", f.JoinPath("b")) 731 manC := f.newDockerBuildManifestWithBuildPath("manC", f.JoinPath("c")) 732 f.Start([]model.Manifest{manA, manB, manC}) 733 f.completeAndCheckBuildsForManifests(manA, manB, manC) 734 735 // start builds for all manifests (we only have 2 build slots) 736 f.SetNextBuildError(errA) 737 f.editFileAndWaitForManifestBuilding("manA", "a/main.go") 738 f.SetNextBuildError(errB) 739 f.editFileAndWaitForManifestBuilding("manB", "b/main.go") 740 f.editFileAndAssertManifestNotBuilding("manC", "c/main.go") 741 742 // Complete one build... 743 f.completeBuildForManifest(manA) 744 call := f.nextCall("expect manA build complete") 745 f.assertCallIsForManifestAndFiles(call, manA, "a/main.go") 746 f.WaitUntilManifestState("last manA build reflects expected error", "manA", func(ms store.ManifestState) bool { 747 return ms.LastBuild().Error == errA 748 }) 749 750 // ...'manC' should start building, even though the manA build ended with an error 751 f.waitUntilManifestBuilding("manC") 752 753 // complete the rest 754 f.completeAndCheckBuildsForManifests(manB, manC) 755 f.WaitUntilManifestState("last manB build reflects expected error", "manB", func(ms store.ManifestState) bool { 756 return ms.LastBuild().Error == errB 757 }) 758 f.WaitUntilManifestState("last manC build recorded and has no error", "manC", func(ms store.ManifestState) bool { 759 return len(ms.BuildHistory) == 2 && ms.LastBuild().Error == nil 760 }) 761 762 err := f.Stop() 763 assert.NoError(t, err) 764 f.assertAllBuildsConsumed() 765 } 766 767 func TestManifestsWithSameTwoImages(t *testing.T) { 768 f := newTestFixture(t) 769 m1, m2 := NewManifestsWithSameTwoImages(f) 770 f.Start([]model.Manifest{m1, m2}) 771 772 f.waitForCompletedBuildCount(2) 773 774 call := f.nextCall("m1 build1") 775 assert.Equal(t, m1.K8sTarget(), call.k8s()) 776 777 call = f.nextCall("m2 build1") 778 assert.Equal(t, m2.K8sTarget(), call.k8s()) 779 780 aPath := f.JoinPath("common", "a.txt") 781 f.fsWatcher.Events <- watch.NewFileEvent(aPath) 782 783 f.waitForCompletedBuildCount(4) 784 785 // Make sure that both builds are triggered, and that they 786 // are triggered in a particular order. 787 call = f.nextCall("m1 build2") 788 assert.Equal(t, m1.K8sTarget(), call.k8s()) 789 790 state := call.state[m1.ImageTargets[0].ID()] 791 assert.Equal(t, map[string]bool{aPath: true}, state.FilesChangedSet) 792 793 // Make sure that when the second build is triggered, we did the bookkeeping 794 // correctly around marking the first and second image built and only deploying 795 // the k8s resources. 796 call = f.nextCall("m2 build2") 797 assert.Equal(t, m2.K8sTarget(), call.k8s()) 798 799 id := m2.ImageTargets[0].ID() 800 result := f.b.resultsByID[id] 801 assert.Equal(t, result, call.state[id].LastResult) 802 assert.Equal(t, 0, len(call.state[id].FilesChangedSet)) 803 804 id = m2.ImageTargets[1].ID() 805 result = f.b.resultsByID[id] 806 assert.Equal(t, result, call.state[id].LastResult) 807 assert.Equal(t, 0, len(call.state[id].FilesChangedSet)) 808 809 err := f.Stop() 810 assert.NoError(t, err) 811 f.assertAllBuildsConsumed() 812 } 813 814 func TestManifestsWithTwoCommonAncestors(t *testing.T) { 815 f := newTestFixture(t) 816 m1, m2 := NewManifestsWithTwoCommonAncestors(f) 817 f.Start([]model.Manifest{m1, m2}) 818 819 f.waitForCompletedBuildCount(2) 820 821 call := f.nextCall("m1 build1") 822 assert.Equal(t, m1.K8sTarget(), call.k8s()) 823 824 call = f.nextCall("m2 build1") 825 assert.Equal(t, m2.K8sTarget(), call.k8s()) 826 827 aPath := f.JoinPath("base", "a.txt") 828 f.fsWatcher.Events <- watch.NewFileEvent(aPath) 829 830 f.waitForCompletedBuildCount(4) 831 832 // Make sure that both builds are triggered, and that they 833 // are triggered in a particular order. 834 call = f.nextCall("m1 build2") 835 assert.Equal(t, m1.K8sTarget(), call.k8s()) 836 837 state := call.state[m1.ImageTargets[0].ID()] 838 assert.Equal(t, map[string]bool{aPath: true}, state.FilesChangedSet) 839 840 // Make sure that when the second build is triggered, we did the bookkeeping 841 // correctly around marking the first and second image built, and only 842 // rebuilding the third image and k8s deploy. 843 call = f.nextCall("m2 build2") 844 assert.Equal(t, m2.K8sTarget(), call.k8s()) 845 846 id := m2.ImageTargets[0].ID() 847 result := f.b.resultsByID[id] 848 assert.Equal(t, result, call.state[id].LastResult) 849 assert.Equal(t, 0, len(call.state[id].FilesChangedSet)) 850 851 id = m2.ImageTargets[1].ID() 852 result = f.b.resultsByID[id] 853 assert.Equal(t, result, call.state[id].LastResult) 854 assert.Equal(t, 0, len(call.state[id].FilesChangedSet)) 855 856 id = m2.ImageTargets[2].ID() 857 result = f.b.resultsByID[id] 858 859 // Assert the 3rd image was not reused from the previous build. 860 assert.NotEqual(t, result, call.state[id].LastResult) 861 assert.Equal(t, 862 map[model.TargetID]bool{m2.ImageTargets[1].ID(): true}, 863 call.state[id].DepsChangedSet) 864 865 err := f.Stop() 866 assert.NoError(t, err) 867 f.assertAllBuildsConsumed() 868 } 869 870 func TestLocalDependsOnNonWorkloadK8s(t *testing.T) { 871 f := newTestFixture(t) 872 873 local1 := manifestbuilder.New(f, "local"). 874 WithLocalResource("exec-local", nil). 875 WithResourceDeps("k8s1"). 876 Build() 877 k8s1 := manifestbuilder.New(f, "k8s1"). 878 WithK8sYAML(testyaml.SanchoYAML). 879 WithK8sPodReadiness(model.PodReadinessIgnore). 880 Build() 881 f.Start([]model.Manifest{local1, k8s1}) 882 883 f.waitForCompletedBuildCount(2) 884 885 call := f.nextCall("k8s1 build") 886 assert.Equal(t, k8s1.K8sTarget(), call.k8s()) 887 888 call = f.nextCall("local build") 889 assert.Equal(t, local1.LocalTarget(), call.local()) 890 891 err := f.Stop() 892 assert.NoError(t, err) 893 f.assertAllBuildsConsumed() 894 } 895 896 func TestManifestsWithCommonAncestorAndTrigger(t *testing.T) { 897 f := newTestFixture(t) 898 m1, m2 := NewManifestsWithCommonAncestor(f) 899 f.Start([]model.Manifest{m1, m2}) 900 901 f.waitForCompletedBuildCount(2) 902 903 call := f.nextCall("m1 build1") 904 assert.Equal(t, m1.K8sTarget(), call.k8s()) 905 906 call = f.nextCall("m2 build1") 907 assert.Equal(t, m2.K8sTarget(), call.k8s()) 908 909 f.store.Dispatch(store.AppendToTriggerQueueAction{Name: m1.Name}) 910 f.waitForCompletedBuildCount(3) 911 912 // Make sure that only one build was triggered. 913 call = f.nextCall("m1 build2") 914 assert.Equal(t, m1.K8sTarget(), call.k8s()) 915 916 f.assertNoCall("m2 should not be rebuilt") 917 918 err := f.Stop() 919 assert.NoError(t, err) 920 f.assertAllBuildsConsumed() 921 } 922 923 func TestDisablingCancelsBuild(t *testing.T) { 924 f := newTestFixture(t) 925 manifest := manifestbuilder.New(f, "local"). 926 WithLocalResource("sleep 10000", nil). 927 Build() 928 f.b.completeBuildsManually = true 929 930 f.Start([]model.Manifest{manifest}) 931 f.waitUntilManifestBuilding("local") 932 933 ds := manifest.DeployTarget.(model.LocalTarget).ServeCmdDisableSource 934 err := configmap.UpsertDisableConfigMap(f.ctx, f.ctrlClient, ds.ConfigMap.Name, ds.ConfigMap.Key, true) 935 require.NoError(t, err) 936 937 f.waitForCompletedBuildCount(1) 938 939 f.withManifestState("local", func(ms store.ManifestState) { 940 require.EqualError(t, ms.LastBuild().Error, "build canceled") 941 }) 942 943 err = f.Stop() 944 require.NoError(t, err) 945 } 946 947 func TestCancelButton(t *testing.T) { 948 f := newTestFixture(t) 949 f.b.completeBuildsManually = true 950 f.useRealTiltfileLoader() 951 f.WriteFile("Tiltfile", ` 952 local_resource('local', 'sleep 10000') 953 `) 954 f.loadAndStart() 955 f.waitUntilManifestBuilding("local") 956 957 var cancelButton v1alpha1.UIButton 958 err := f.ctrlClient.Get(f.ctx, types.NamespacedName{Name: uibutton.StopBuildButtonName("local")}, &cancelButton) 959 require.NoError(t, err) 960 cancelButton.Status.LastClickedAt = metav1.NowMicro() 961 err = f.ctrlClient.Status().Update(f.ctx, &cancelButton) 962 require.NoError(t, err) 963 964 f.waitForCompletedBuildCount(1) 965 966 f.withManifestState("local", func(ms store.ManifestState) { 967 require.EqualError(t, ms.LastBuild().Error, "build canceled") 968 }) 969 970 err = f.Stop() 971 require.NoError(t, err) 972 } 973 974 func TestCancelButtonClickedBeforeBuild(t *testing.T) { 975 f := newTestFixture(t) 976 f.b.completeBuildsManually = true 977 f.useRealTiltfileLoader() 978 f.WriteFile("Tiltfile", ` 979 local_resource('local', 'sleep 10000') 980 `) 981 // grab a timestamp now to represent clicking the button before the build started 982 ts := metav1.NowMicro() 983 984 f.loadAndStart() 985 f.waitUntilManifestBuilding("local") 986 987 var cancelButton v1alpha1.UIButton 988 err := f.ctrlClient.Get(f.ctx, types.NamespacedName{Name: uibutton.StopBuildButtonName("local")}, &cancelButton) 989 require.NoError(t, err) 990 cancelButton.Status.LastClickedAt = ts 991 err = f.ctrlClient.Status().Update(f.ctx, &cancelButton) 992 require.NoError(t, err) 993 994 // give the build controller a little time to process the button click 995 require.Never(t, func() bool { 996 state := f.store.RLockState() 997 defer f.store.RUnlockState() 998 return state.CompletedBuildCount > 0 999 }, 20*time.Millisecond, 2*time.Millisecond, "build finished on its own even though manual build completion is enabled") 1000 1001 f.b.completeBuild("local:local") 1002 1003 f.waitForCompletedBuildCount(1) 1004 1005 f.withManifestState("local", func(ms store.ManifestState) { 1006 require.NoError(t, ms.LastBuild().Error) 1007 }) 1008 1009 err = f.Stop() 1010 require.NoError(t, err) 1011 } 1012 1013 func TestBuildControllerK8sFileDependencies(t *testing.T) { 1014 f := newTestFixture(t) 1015 1016 kt := k8s.MustTarget("fe", testyaml.SanchoYAML). 1017 WithPathDependencies([]string{f.JoinPath("k8s-dep")}). 1018 WithIgnores([]v1alpha1.IgnoreDef{ 1019 {BasePath: f.JoinPath("k8s-dep", ".git")}, 1020 { 1021 BasePath: f.JoinPath("k8s-dep"), 1022 Patterns: []string{"ignore-me"}, 1023 }, 1024 }) 1025 m := model.Manifest{Name: "fe"}.WithDeployTarget(kt) 1026 1027 f.Start([]model.Manifest{m}) 1028 1029 call := f.nextCall() 1030 assert.Empty(t, call.k8sState().FilesChanged()) 1031 1032 // path dependency is on ./k8s-dep/** with a local repo of ./k8s-dep/.git/** (ignored) 1033 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("k8s-dep", "ignore-me")) 1034 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("k8s-dep", ".git", "file")) 1035 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath("k8s-dep", "file")) 1036 1037 call = f.nextCall() 1038 assert.Equal(t, []string{f.JoinPath("k8s-dep", "file")}, call.k8sState().FilesChanged()) 1039 1040 err := f.Stop() 1041 assert.NoError(t, err) 1042 f.assertAllBuildsConsumed() 1043 } 1044 1045 func (f *testFixture) waitUntilManifestBuilding(name model.ManifestName) { 1046 f.t.Helper() 1047 msg := fmt.Sprintf("manifest %q is building", name) 1048 f.WaitUntilManifestState(msg, name, func(ms store.ManifestState) bool { 1049 return ms.IsBuilding() 1050 }) 1051 1052 f.withState(func(st store.EngineState) { 1053 ok := st.CurrentBuildSet[name] 1054 require.True(f.t, ok, "expected EngineState to reflect that %q is currently building", name) 1055 }) 1056 } 1057 1058 func (f *testFixture) waitUntilManifestNotBuilding(name model.ManifestName) { 1059 msg := fmt.Sprintf("manifest %q is NOT building", name) 1060 f.WaitUntilManifestState(msg, name, func(ms store.ManifestState) bool { 1061 return !ms.IsBuilding() 1062 }) 1063 1064 f.withState(func(st store.EngineState) { 1065 ok := st.CurrentBuildSet[name] 1066 require.False(f.t, ok, "expected EngineState to reflect that %q is NOT currently building", name) 1067 }) 1068 } 1069 1070 func (f *testFixture) waitUntilNumBuildSlots(expected int) { 1071 msg := fmt.Sprintf("%d build slots available", expected) 1072 f.WaitUntil(msg, func(st store.EngineState) bool { 1073 return expected == st.AvailableBuildSlots() 1074 }) 1075 } 1076 1077 func (f *testFixture) editFileAndWaitForManifestBuilding(name model.ManifestName, path string) { 1078 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath(path)) 1079 f.waitUntilManifestBuilding(name) 1080 } 1081 1082 func (f *testFixture) editFileAndAssertManifestNotBuilding(name model.ManifestName, path string) { 1083 f.fsWatcher.Events <- watch.NewFileEvent(f.JoinPath(path)) 1084 f.waitUntilManifestNotBuilding(name) 1085 } 1086 1087 func (f *testFixture) assertCallIsForManifestAndFiles(call buildAndDeployCall, m model.Manifest, files ...string) { 1088 assert.Equal(f.t, m.ImageTargetAt(0).ID(), call.firstImgTarg().ID()) 1089 assert.Equal(f.t, f.JoinPaths(files), call.oneImageState().FilesChanged()) 1090 } 1091 1092 func (f *testFixture) completeAndCheckBuildsForManifests(manifests ...model.Manifest) { 1093 for _, m := range manifests { 1094 f.completeBuildForManifest(m) 1095 } 1096 1097 expectedImageTargets := make([][]model.ImageTarget, len(manifests)) 1098 var actualImageTargets [][]model.ImageTarget 1099 for i, m := range manifests { 1100 expectedImageTargets[i] = m.ImageTargets 1101 1102 call := f.nextCall("timed out waiting for call %d/%d", i+1, len(manifests)) 1103 actualImageTargets = append(actualImageTargets, call.imageTargets()) 1104 } 1105 require.ElementsMatch(f.t, expectedImageTargets, actualImageTargets) 1106 1107 for _, m := range manifests { 1108 f.waitUntilManifestNotBuilding(m.Name) 1109 } 1110 } 1111 1112 func (f *testFixture) simpleManifestWithTriggerMode(name model.ManifestName, tm model.TriggerMode) model.Manifest { 1113 return manifestbuilder.New(f, name).WithTriggerMode(tm). 1114 WithImageTarget(NewSanchoDockerBuildImageTarget(f)). 1115 WithK8sYAML(SanchoYAML).Build() 1116 }