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