github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/tiltfile/reconciler_test.go (about)

     1  package tiltfile
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"path/filepath"
     8  	"reflect"
     9  	"strconv"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/types"
    18  	"k8s.io/client-go/util/workqueue"
    19  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    20  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    21  
    22  	"github.com/tilt-dev/tilt/internal/container"
    23  	configmap2 "github.com/tilt-dev/tilt/internal/controllers/apis/configmap"
    24  	"github.com/tilt-dev/tilt/internal/controllers/apis/uibutton"
    25  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    26  	"github.com/tilt-dev/tilt/internal/docker"
    27  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    28  	"github.com/tilt-dev/tilt/internal/store"
    29  	"github.com/tilt-dev/tilt/internal/testutils/configmap"
    30  	"github.com/tilt-dev/tilt/internal/testutils/manifestbuilder"
    31  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    32  	"github.com/tilt-dev/tilt/internal/tiltfile"
    33  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    34  	"github.com/tilt-dev/tilt/pkg/model"
    35  	"github.com/tilt-dev/wmclient/pkg/analytics"
    36  )
    37  
    38  func TestDefault(t *testing.T) {
    39  	f := newFixture(t)
    40  	p := f.tempdir.JoinPath("Tiltfile")
    41  	f.tempdir.WriteFile(p, "print('hello-world')")
    42  
    43  	tf := v1alpha1.Tiltfile{
    44  		ObjectMeta: metav1.ObjectMeta{
    45  			Name: "my-tf",
    46  		},
    47  		Spec: v1alpha1.TiltfileSpec{
    48  			Path: p,
    49  		},
    50  	}
    51  	ts := time.Now()
    52  	f.Create(&tf)
    53  
    54  	// Make sure the FileWatch was created
    55  	var fw v1alpha1.FileWatch
    56  	fwKey := types.NamespacedName{Name: "configs:my-tf"}
    57  	f.MustGet(fwKey, &fw)
    58  	assert.Equal(t, tf.Spec.Path, fw.Spec.WatchedPaths[0])
    59  
    60  	f.waitForRunning(tf.Name)
    61  
    62  	f.popQueue()
    63  
    64  	f.waitForTerminatedAfter(tf.Name, ts)
    65  
    66  	f.Delete(&tf)
    67  
    68  	// Ensure the FileWatch was deleted.
    69  	assert.False(t, f.Get(fwKey, &fw))
    70  }
    71  
    72  func TestSteadyState(t *testing.T) {
    73  	f := newFixture(t)
    74  	p := f.tempdir.JoinPath("Tiltfile")
    75  	f.tempdir.WriteFile(p, "print('hello-world')")
    76  
    77  	tf := v1alpha1.Tiltfile{
    78  		ObjectMeta: metav1.ObjectMeta{
    79  			Name: "my-tf",
    80  		},
    81  		Spec: v1alpha1.TiltfileSpec{
    82  			Path: p,
    83  		},
    84  	}
    85  	f.createAndWaitForLoaded(&tf)
    86  
    87  	// Make sure a second reconcile doesn't update the status again.
    88  	var tf2 = v1alpha1.Tiltfile{}
    89  	f.MustReconcile(types.NamespacedName{Name: "my-tf"})
    90  	f.MustGet(types.NamespacedName{Name: "my-tf"}, &tf2)
    91  	assert.Equal(t, tf.ResourceVersion, tf2.ResourceVersion)
    92  }
    93  
    94  func TestLiveUpdate(t *testing.T) {
    95  	f := newFixture(t)
    96  	p := f.tempdir.JoinPath("Tiltfile")
    97  
    98  	luSpec := v1alpha1.LiveUpdateSpec{
    99  		BasePath:  f.tempdir.Path(),
   100  		StopPaths: []string{filepath.Join("src", "package.json")},
   101  		Syncs:     []v1alpha1.LiveUpdateSync{{LocalPath: "src", ContainerPath: "/src"}},
   102  	}
   103  	expectedSpec := *(luSpec.DeepCopy())
   104  	expectedSpec.Sources = []v1alpha1.LiveUpdateSource{{
   105  		FileWatch: "image:sancho-image",
   106  		ImageMap:  "sancho-image",
   107  	}}
   108  	expectedSpec.Selector.Kubernetes = &v1alpha1.LiveUpdateKubernetesSelector{
   109  		ImageMapName:  "sancho-image",
   110  		DiscoveryName: "sancho",
   111  		ApplyName:     "sancho",
   112  	}
   113  
   114  	sanchoImage := model.MustNewImageTarget(container.MustParseSelector("sancho-image")).
   115  		WithLiveUpdateSpec("sancho:sancho-image", luSpec).
   116  		WithDockerImage(v1alpha1.DockerImageSpec{Context: f.tempdir.Path()})
   117  	sancho := manifestbuilder.New(f.tempdir, "sancho").
   118  		WithImageTargets(sanchoImage).
   119  		WithK8sYAML(testyaml.SanchoYAML).
   120  		Build()
   121  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   122  		Manifests: []model.Manifest{sancho},
   123  	}
   124  
   125  	tf := v1alpha1.Tiltfile{
   126  		ObjectMeta: metav1.ObjectMeta{
   127  			Name: "my-tf",
   128  		},
   129  		Spec: v1alpha1.TiltfileSpec{
   130  			Path: p,
   131  		},
   132  	}
   133  	f.createAndWaitForLoaded(&tf)
   134  
   135  	assert.Equal(t, "", tf.Status.Terminated.Error)
   136  
   137  	var luList = v1alpha1.LiveUpdateList{}
   138  	f.List(&luList)
   139  	if assert.Equal(t, 1, len(luList.Items)) {
   140  		assert.Equal(t, "sancho:sancho-image", luList.Items[0].Name)
   141  		assert.Equal(t, expectedSpec, luList.Items[0].Spec)
   142  	}
   143  }
   144  
   145  func TestCluster(t *testing.T) {
   146  	f := newFixture(t)
   147  	p := f.tempdir.JoinPath("Tiltfile")
   148  	f.r.k8sContextOverride = "context-override"
   149  	f.r.k8sNamespaceOverride = "namespace-override"
   150  
   151  	expected := &v1alpha1.ClusterConnection{
   152  		Kubernetes: &v1alpha1.KubernetesClusterConnection{
   153  			Context:   string(f.r.k8sContextOverride),
   154  			Namespace: string(f.r.k8sNamespaceOverride),
   155  		},
   156  	}
   157  
   158  	sancho := manifestbuilder.New(f.tempdir, "sancho").
   159  		WithK8sYAML(testyaml.SanchoYAML).
   160  		Build()
   161  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   162  		Manifests: []model.Manifest{sancho},
   163  	}
   164  
   165  	name := model.MainTiltfileManifestName.String()
   166  	tf := v1alpha1.Tiltfile{
   167  		ObjectMeta: metav1.ObjectMeta{
   168  			Name: name,
   169  		},
   170  		Spec: v1alpha1.TiltfileSpec{
   171  			Path: p,
   172  		},
   173  	}
   174  	f.createAndWaitForLoaded(&tf)
   175  
   176  	assert.Equal(t, "", tf.Status.Terminated.Error)
   177  
   178  	var clList = v1alpha1.ClusterList{}
   179  	f.List(&clList)
   180  	if assert.Equal(t, 1, len(clList.Items)) {
   181  		assert.Equal(t, "default", clList.Items[0].Name)
   182  		assert.Equal(t, expected, clList.Items[0].Spec.Connection)
   183  	}
   184  }
   185  
   186  func TestLocalServe(t *testing.T) {
   187  	f := newFixture(t)
   188  	p := f.tempdir.JoinPath("Tiltfile")
   189  
   190  	m := manifestbuilder.New(f.tempdir, "foo").WithLocalServeCmd(".").Build()
   191  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   192  		Manifests: []model.Manifest{m},
   193  	}
   194  
   195  	tf := v1alpha1.Tiltfile{
   196  		ObjectMeta: metav1.ObjectMeta{
   197  			Name: "my-tf",
   198  		},
   199  		Spec: v1alpha1.TiltfileSpec{
   200  			Path: p,
   201  		},
   202  	}
   203  	f.createAndWaitForLoaded(&tf)
   204  
   205  	assert.Equal(t, "", tf.Status.Terminated.Error)
   206  
   207  	a := f.st.WaitForAction(t, reflect.TypeOf(ConfigsReloadedAction{})).(ConfigsReloadedAction)
   208  	require.Equal(t, 1, len(a.Manifests))
   209  	m = a.Manifests[0]
   210  	require.Equal(t, model.ManifestName("foo"), m.Name)
   211  	require.IsType(t, model.LocalTarget{}, m.DeployTarget)
   212  	lt := m.DeployTarget.(model.LocalTarget)
   213  	require.NotNil(t, lt.ServeCmdDisableSource, "ServeCmdDisableSource is nil")
   214  	require.NotNil(t, lt.ServeCmdDisableSource.ConfigMap, "ServeCmdDisableSource.ConfigMap is nil")
   215  	require.Equal(t, "foo-disable", lt.ServeCmdDisableSource.ConfigMap.Name)
   216  }
   217  
   218  func TestDockerMetrics(t *testing.T) {
   219  	f := newFixture(t)
   220  	p := f.tempdir.JoinPath("Tiltfile")
   221  
   222  	sanchoImage := model.MustNewImageTarget(container.MustParseSelector("sancho-image")).
   223  		WithDockerImage(v1alpha1.DockerImageSpec{Context: f.tempdir.Path()})
   224  	sancho := manifestbuilder.New(f.tempdir, "sancho").
   225  		WithImageTargets(sanchoImage).
   226  		WithK8sYAML(testyaml.SanchoYAML).
   227  		Build()
   228  
   229  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   230  		Manifests: []model.Manifest{sancho},
   231  	}
   232  
   233  	tf := v1alpha1.Tiltfile{
   234  		ObjectMeta: metav1.ObjectMeta{
   235  			Name: "my-tf",
   236  		},
   237  		Spec: v1alpha1.TiltfileSpec{
   238  			Path: p,
   239  		},
   240  	}
   241  	f.createAndWaitForLoaded(&tf)
   242  
   243  	connectEvt := analytics.CountEvent{
   244  		Name: "api.tiltfile.docker.connect",
   245  		Tags: map[string]string{
   246  			"server.arch":    "amd64",
   247  			"server.version": "20.10.11",
   248  			"status":         "connected",
   249  		},
   250  		N: 1,
   251  	}
   252  	assert.ElementsMatch(t, []analytics.CountEvent{connectEvt}, f.ma.Counts)
   253  }
   254  
   255  func TestArgsChangeResetsEnabledResources(t *testing.T) {
   256  	f := newFixture(t)
   257  	p := f.tempdir.JoinPath("Tiltfile")
   258  
   259  	m1 := manifestbuilder.New(f.tempdir, "m1").WithLocalServeCmd("hi").Build()
   260  	m2 := manifestbuilder.New(f.tempdir, "m2").WithLocalServeCmd("hi").Build()
   261  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   262  		Manifests:        []model.Manifest{m1, m2},
   263  		EnabledManifests: []model.ManifestName{"m1", "m2"},
   264  	}
   265  
   266  	tf := v1alpha1.Tiltfile{
   267  		ObjectMeta: metav1.ObjectMeta{
   268  			Name: "my-tf",
   269  		},
   270  		Spec: v1alpha1.TiltfileSpec{
   271  			Path: p,
   272  			Args: []string{"m1", "m2"},
   273  		},
   274  	}
   275  	f.createAndWaitForLoaded(&tf)
   276  
   277  	ts := time.Now()
   278  
   279  	f.setArgs("my-tf", []string{"m2"})
   280  	f.tfl.Result.EnabledManifests = []model.ManifestName{"m2"}
   281  
   282  	f.MustReconcile(types.NamespacedName{Name: "my-tf"})
   283  	f.waitForRunning("my-tf")
   284  	f.popQueue()
   285  	f.waitForTerminatedAfter("my-tf", ts)
   286  
   287  	f.requireEnabled(m1, false)
   288  	f.requireEnabled(m2, true)
   289  }
   290  
   291  func TestRunWithoutArgsChangePreservesEnabledResources(t *testing.T) {
   292  	f := newFixture(t)
   293  	p := f.tempdir.JoinPath("Tiltfile")
   294  
   295  	m1 := manifestbuilder.New(f.tempdir, "m1").WithLocalServeCmd("hi").Build()
   296  	m2 := manifestbuilder.New(f.tempdir, "m2").WithLocalServeCmd("hi").Build()
   297  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   298  		Manifests:        []model.Manifest{m1, m2},
   299  		EnabledManifests: []model.ManifestName{"m1", "m2"},
   300  	}
   301  
   302  	tf := v1alpha1.Tiltfile{
   303  		ObjectMeta: metav1.ObjectMeta{
   304  			Name: "my-tf",
   305  		},
   306  		Spec: v1alpha1.TiltfileSpec{
   307  			Path: p,
   308  			Args: []string{"m1"},
   309  		},
   310  	}
   311  	f.createAndWaitForLoaded(&tf)
   312  
   313  	err := configmap.UpsertDisableConfigMap(f.Context(), f.Client, "m2-disable", "isDisabled", false)
   314  	require.NoError(t, err)
   315  
   316  	f.setArgs("my-tf", tf.Spec.Args)
   317  
   318  	f.triggerRun("my-tf")
   319  
   320  	ts := time.Now()
   321  	f.MustReconcile(types.NamespacedName{Name: "my-tf"})
   322  	f.waitForRunning("my-tf")
   323  	f.popQueue()
   324  	f.waitForTerminatedAfter("my-tf", ts)
   325  
   326  	f.requireEnabled(m1, true)
   327  	f.requireEnabled(m2, true)
   328  }
   329  
   330  func TestTiltfileFailurePreservesEnabledResources(t *testing.T) {
   331  	f := newFixture(t)
   332  	p := f.tempdir.JoinPath("Tiltfile")
   333  
   334  	m1 := manifestbuilder.New(f.tempdir, "m1").WithLocalServeCmd("hi").Build()
   335  	m2 := manifestbuilder.New(f.tempdir, "m2").WithLocalServeCmd("hi").Build()
   336  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   337  		Manifests:        []model.Manifest{m1, m2},
   338  		EnabledManifests: []model.ManifestName{"m1"},
   339  	}
   340  
   341  	tf := v1alpha1.Tiltfile{
   342  		ObjectMeta: metav1.ObjectMeta{
   343  			Name: "my-tf",
   344  		},
   345  		Spec: v1alpha1.TiltfileSpec{
   346  			Path: p,
   347  			Args: []string{"m1"},
   348  		},
   349  	}
   350  	f.createAndWaitForLoaded(&tf)
   351  
   352  	f.tfl.Result = tiltfile.TiltfileLoadResult{
   353  		Manifests:        []model.Manifest{m1, m2},
   354  		EnabledManifests: []model.ManifestName{},
   355  		Error:            errors.New("unknown manifest: m3"),
   356  	}
   357  
   358  	f.triggerRun("my-tf")
   359  
   360  	ts := time.Now()
   361  	f.MustReconcile(types.NamespacedName{Name: "my-tf"})
   362  	f.waitForRunning("my-tf")
   363  	f.popQueue()
   364  	f.waitForTerminatedAfter("my-tf", ts)
   365  
   366  	f.requireEnabled(m1, true)
   367  	f.requireEnabled(m2, false)
   368  }
   369  
   370  func TestCancel(t *testing.T) {
   371  	f := newFixture(t)
   372  	p := f.tempdir.JoinPath("Tiltfile")
   373  	f.tempdir.WriteFile(p, "print('hello-world')")
   374  
   375  	f.tfl.Delegate = newBlockingTiltfileLoader()
   376  
   377  	tf := v1alpha1.Tiltfile{
   378  		ObjectMeta: metav1.ObjectMeta{
   379  			Name: "my-tf",
   380  		},
   381  		Spec: v1alpha1.TiltfileSpec{
   382  			Path:   p,
   383  			StopOn: &v1alpha1.StopOnSpec{UIButtons: []string{uibutton.StopBuildButtonName("my-tf")}},
   384  		},
   385  	}
   386  
   387  	cancelButton := uibutton.StopBuildButton(tf.Name)
   388  	err := f.Client.Create(f.Context(), cancelButton)
   389  	require.NoError(t, err)
   390  
   391  	ts := time.Now()
   392  	f.Create(&tf)
   393  
   394  	f.waitForRunning(tf.Name)
   395  
   396  	cancelButton.Status.LastClickedAt = metav1.NowMicro()
   397  	f.UpdateStatus(cancelButton)
   398  	require.NoError(t, err)
   399  
   400  	f.MustReconcile(types.NamespacedName{Name: tf.Name})
   401  
   402  	f.popQueue()
   403  
   404  	f.waitForTerminatedAfter(tf.Name, ts)
   405  
   406  	f.Get(types.NamespacedName{Name: tf.Name}, &tf)
   407  	require.NotNil(t, tf.Status.Terminated)
   408  	require.Equal(t, "build canceled", tf.Status.Terminated.Error)
   409  }
   410  
   411  func TestCancelClickedBeforeLoad(t *testing.T) {
   412  	f := newFixture(t)
   413  	p := f.tempdir.JoinPath("Tiltfile")
   414  	f.tempdir.WriteFile(p, "print('hello-world')")
   415  
   416  	tfl := newBlockingTiltfileLoader()
   417  	f.tfl.Delegate = tfl
   418  
   419  	tf := v1alpha1.Tiltfile{
   420  		ObjectMeta: metav1.ObjectMeta{
   421  			Name: "my-tf",
   422  		},
   423  		Spec: v1alpha1.TiltfileSpec{
   424  			Path:   p,
   425  			StopOn: &v1alpha1.StopOnSpec{UIButtons: []string{uibutton.StopBuildButtonName("my-tf")}},
   426  		},
   427  	}
   428  
   429  	cancelButton := uibutton.StopBuildButton(tf.Name)
   430  	cancelButton.Status.LastClickedAt = metav1.NewMicroTime(time.Now().Add(-time.Second))
   431  	err := f.Client.Create(f.Context(), cancelButton)
   432  	require.NoError(t, err)
   433  
   434  	nn := types.NamespacedName{Name: tf.Name}
   435  
   436  	ts := time.Now()
   437  	f.Create(&tf)
   438  
   439  	f.waitForRunning(tf.Name)
   440  
   441  	// give the reconciler a chance to observe the cancel button click
   442  	f.MustReconcile(nn)
   443  
   444  	// finish the build
   445  	tfl.Complete()
   446  
   447  	f.MustReconcile(nn)
   448  
   449  	f.popQueue()
   450  
   451  	f.waitForTerminatedAfter(tf.Name, ts)
   452  
   453  	f.Get(nn, &tf)
   454  	require.NotNil(t, tf.Status.Terminated)
   455  	require.Equal(t, "", tf.Status.Terminated.Error)
   456  }
   457  
   458  type testStore struct {
   459  	*store.TestingStore
   460  	out *bytes.Buffer
   461  }
   462  
   463  func NewTestingStore() *testStore {
   464  	return &testStore{
   465  		TestingStore: store.NewTestingStore(),
   466  		out:          bytes.NewBuffer(nil),
   467  	}
   468  }
   469  
   470  func (s *testStore) Dispatch(action store.Action) {
   471  	s.TestingStore.Dispatch(action)
   472  
   473  	logAction, ok := action.(store.LogAction)
   474  	if ok {
   475  		_, _ = fmt.Fprintf(s.out, "%s", logAction.Message())
   476  	}
   477  }
   478  
   479  type fixture struct {
   480  	*fake.ControllerFixture
   481  	tempdir *tempdir.TempDirFixture
   482  	st      *testStore
   483  	r       *Reconciler
   484  	q       workqueue.RateLimitingInterface
   485  	tfl     *tiltfile.FakeTiltfileLoader
   486  	ma      *analytics.MemoryAnalytics
   487  }
   488  
   489  func newFixture(t *testing.T) *fixture {
   490  	cfb := fake.NewControllerFixtureBuilder(t)
   491  	tf := tempdir.NewTempDirFixture(t)
   492  
   493  	st := NewTestingStore()
   494  	tfl := tiltfile.NewFakeTiltfileLoader()
   495  	d := docker.NewFakeClient()
   496  	r := NewReconciler(st, tfl, d, cfb.Client, v1alpha1.NewScheme(), store.EngineModeUp, "", "", 0)
   497  	q := workqueue.NewRateLimitingQueue(
   498  		workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond, time.Millisecond))
   499  	_ = r.requeuer.Start(context.Background(), q)
   500  
   501  	return &fixture{
   502  		ControllerFixture: cfb.Build(r),
   503  		tempdir:           tf,
   504  		st:                st,
   505  		r:                 r,
   506  		q:                 q,
   507  		tfl:               tfl,
   508  		ma:                cfb.Analytics(),
   509  	}
   510  }
   511  
   512  // Wait for the next item on the workqueue, then run reconcile on it.
   513  func (f *fixture) popQueue() {
   514  	f.T().Helper()
   515  
   516  	done := make(chan error)
   517  	go func() {
   518  		item, _ := f.q.Get()
   519  		_, err := f.r.Reconcile(f.Context(), item.(reconcile.Request))
   520  		f.q.Done(item)
   521  		done <- err
   522  	}()
   523  
   524  	select {
   525  	case <-time.After(time.Second):
   526  		f.T().Fatal("timeout waiting for workqueue")
   527  	case err := <-done:
   528  		assert.NoError(f.T(), err)
   529  	}
   530  }
   531  
   532  func (f *fixture) waitForTerminatedAfter(name string, ts time.Time) {
   533  	require.Eventually(f.T(), func() bool {
   534  		var tf v1alpha1.Tiltfile
   535  		f.MustGet(types.NamespacedName{Name: name}, &tf)
   536  		return tf.Status.Terminated != nil && tf.Status.Terminated.FinishedAt.After(ts)
   537  	}, time.Second, time.Millisecond, "waiting for tiltfile to finish running")
   538  }
   539  
   540  func (f *fixture) waitForRunning(name string) {
   541  	require.Eventually(f.T(), func() bool {
   542  		var tf v1alpha1.Tiltfile
   543  		f.MustGet(types.NamespacedName{Name: name}, &tf)
   544  		return tf.Status.Running != nil
   545  	}, time.Second, time.Millisecond, "waiting for tiltfile to start running")
   546  }
   547  
   548  func (f *fixture) createAndWaitForLoaded(tf *v1alpha1.Tiltfile) {
   549  	ts := time.Now()
   550  	f.Create(tf)
   551  
   552  	f.waitForRunning(tf.Name)
   553  
   554  	f.popQueue()
   555  
   556  	f.waitForTerminatedAfter(tf.Name, ts)
   557  
   558  	f.MustGet(types.NamespacedName{Name: tf.Name}, tf)
   559  }
   560  
   561  func (f *fixture) triggerRun(name string) {
   562  	queue := configmap2.TriggerQueueCreate([]configmap2.TriggerQueueEntry{{Name: model.ManifestName(name)}})
   563  	f.Create(&queue)
   564  }
   565  
   566  func (f *fixture) setArgs(name string, args []string) {
   567  	tf := v1alpha1.Tiltfile{
   568  		ObjectMeta: metav1.ObjectMeta{
   569  			Name: name,
   570  		},
   571  	}
   572  	_, err := controllerutil.CreateOrUpdate(f.Context(), f.Client, &tf, func() error {
   573  		tf.Spec.Args = args
   574  		return nil
   575  	})
   576  	require.NoError(f.T(), err)
   577  }
   578  
   579  func (f *fixture) requireEnabled(m model.Manifest, isEnabled bool) {
   580  	var cm v1alpha1.ConfigMap
   581  	f.MustGet(types.NamespacedName{Name: disableConfigMapName(m)}, &cm)
   582  	isDisabled, err := strconv.ParseBool(cm.Data["isDisabled"])
   583  	require.NoError(f.T(), err)
   584  	actualIsEnabled := !isDisabled
   585  	require.Equal(f.T(), isEnabled, actualIsEnabled, "is %s enabled", m.Name)
   586  }
   587  
   588  // builds block until canceled or manually completed
   589  type blockingTiltfileLoader struct {
   590  	completionChan chan struct{}
   591  }
   592  
   593  func newBlockingTiltfileLoader() blockingTiltfileLoader {
   594  	return blockingTiltfileLoader{completionChan: make(chan struct{})}
   595  }
   596  
   597  func (b blockingTiltfileLoader) Load(ctx context.Context, tf *v1alpha1.Tiltfile, prevResult *tiltfile.TiltfileLoadResult) tiltfile.TiltfileLoadResult {
   598  	select {
   599  	case <-ctx.Done():
   600  	case <-b.completionChan:
   601  	}
   602  	return tiltfile.TiltfileLoadResult{}
   603  }
   604  
   605  func (b blockingTiltfileLoader) Complete() {
   606  	close(b.completionChan)
   607  }