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  }