github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/target_queue_test.go (about)

     1  package buildcontrol
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/distribution/reference"
     9  	"github.com/stretchr/testify/assert"
    10  
    11  	"github.com/tilt-dev/tilt/internal/testutils"
    12  
    13  	"github.com/tilt-dev/tilt/internal/container"
    14  	"github.com/tilt-dev/tilt/internal/store"
    15  	"github.com/tilt-dev/tilt/pkg/model"
    16  )
    17  
    18  func TestTargetQueue_Simple(t *testing.T) {
    19  	f := newTargetQueueFixture(t)
    20  
    21  	t1 := newDockerImageTarget("vigoda")
    22  	s1 := store.BuildState{}
    23  
    24  	targets := []model.ImageTarget{t1}
    25  	buildStateSet := store.BuildStateSet{
    26  		t1.ID(): s1,
    27  	}
    28  
    29  	f.run(targets, buildStateSet)
    30  
    31  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{
    32  		t1.ID(): newFakeBuildHandlerCall(t1, 1, []store.ImageBuildResult{}),
    33  	}
    34  	assert.Equal(t, expectedCalls, f.handler.calls)
    35  }
    36  
    37  func TestTargetQueue_DepsBuilt(t *testing.T) {
    38  	f := newTargetQueueFixture(t)
    39  
    40  	fooTarget := newDockerImageTarget("foo")
    41  	s1 := store.BuildState{LastResult: store.NewImageBuildResultSingleRef(fooTarget.ID(), container.MustParseNamedTagged("foo:1234"))}
    42  	barTarget := newDockerImageTarget("bar").
    43  		WithImageMapDeps([]string{fooTarget.ImageMapName()})
    44  	s2 := store.BuildState{}
    45  
    46  	targets := []model.ImageTarget{fooTarget, barTarget}
    47  	buildStateSet := store.BuildStateSet{
    48  		fooTarget.ID(): s1,
    49  		barTarget.ID(): s2,
    50  	}
    51  
    52  	f.run(targets, buildStateSet)
    53  
    54  	ref := container.MustParseNamedTagged(
    55  		store.LocalImageRefFromBuildResult(s1.LastResult))
    56  	barCall := newFakeBuildHandlerCall(barTarget, 1, []store.ImageBuildResult{
    57  		store.NewImageBuildResultSingleRef(fooTarget.ID(), ref),
    58  	})
    59  
    60  	// foo has a valid last result, so only bar gets rebuilt
    61  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{
    62  		barTarget.ID(): barCall,
    63  	}
    64  	assert.Equal(t, expectedCalls, f.handler.calls)
    65  }
    66  
    67  func TestTargetQueue_DepsUnbuilt(t *testing.T) {
    68  	f := newTargetQueueFixture(t)
    69  
    70  	fooTarget := newDockerImageTarget("foo")
    71  	s1 := store.BuildState{}
    72  	barTarget := newDockerImageTarget("bar").
    73  		WithImageMapDeps([]string{fooTarget.ImageMapName()})
    74  	var s2 = store.BuildState{LastResult: store.NewImageBuildResultSingleRef(
    75  		barTarget.ID(),
    76  		container.MustParseNamedTagged("bar:54321"),
    77  	)}
    78  	targets := []model.ImageTarget{fooTarget, barTarget}
    79  	buildStateSet := store.BuildStateSet{
    80  		fooTarget.ID(): s1,
    81  		barTarget.ID(): s2,
    82  	}
    83  
    84  	f.run(targets, buildStateSet)
    85  
    86  	fooCall := newFakeBuildHandlerCall(fooTarget, 1, []store.ImageBuildResult{})
    87  	// bar's dep is dirty, so bar should not get its old state
    88  	barCall := newFakeBuildHandlerCall(barTarget, 2, []store.ImageBuildResult{fooCall.result})
    89  
    90  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{
    91  		fooTarget.ID(): fooCall,
    92  		barTarget.ID(): barCall,
    93  	}
    94  	assert.Equal(t, expectedCalls, f.handler.calls)
    95  }
    96  
    97  func TestTargetQueue_IncrementalBuild(t *testing.T) {
    98  	f := newTargetQueueFixture(t)
    99  
   100  	fooTarget := newDockerImageTarget("foo")
   101  	s1 := store.BuildState{
   102  		LastResult: store.NewImageBuildResultSingleRef(
   103  			fooTarget.ID(),
   104  			container.MustParseNamedTagged("foo:1234"),
   105  		),
   106  		FilesChangedSet: map[string]bool{"hello.txt": true},
   107  	}
   108  
   109  	targets := []model.ImageTarget{fooTarget}
   110  	buildStateSet := store.BuildStateSet{fooTarget.ID(): s1}
   111  
   112  	f.run(targets, buildStateSet)
   113  
   114  	fooCall := newFakeBuildHandlerCall(fooTarget, 1, []store.ImageBuildResult{})
   115  
   116  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{
   117  		fooTarget.ID(): fooCall,
   118  	}
   119  	assert.Equal(t, expectedCalls, f.handler.calls)
   120  }
   121  
   122  func TestTargetQueue_CachedBuild(t *testing.T) {
   123  	f := newTargetQueueFixture(t)
   124  
   125  	fooTarget := newDockerImageTarget("foo")
   126  	s1 := store.BuildState{
   127  		LastResult: store.NewImageBuildResultSingleRef(
   128  			fooTarget.ID(),
   129  			container.MustParseNamedTagged("foo:1234"),
   130  		),
   131  	}
   132  
   133  	targets := []model.ImageTarget{fooTarget}
   134  	buildStateSet := store.BuildStateSet{fooTarget.ID(): s1}
   135  
   136  	f.run(targets, buildStateSet)
   137  
   138  	// last result is still valid, so handler doesn't get called at all
   139  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{}
   140  	assert.Equal(t, expectedCalls, f.handler.calls)
   141  }
   142  
   143  func TestTargetQueue_DepsBuiltButReaped(t *testing.T) {
   144  	f := newTargetQueueFixture(t)
   145  
   146  	fooTarget := newDockerImageTarget("foo")
   147  	s1 := store.BuildState{LastResult: store.NewImageBuildResultSingleRef(fooTarget.ID(), container.MustParseNamedTagged("foo:1234"))}
   148  	barTarget := newDockerImageTarget("bar").
   149  		WithImageMapDeps([]string{fooTarget.ImageMapName()})
   150  	s2 := store.BuildState{}
   151  
   152  	targets := []model.ImageTarget{fooTarget, barTarget}
   153  	buildStateSet := store.BuildStateSet{
   154  		fooTarget.ID(): s1,
   155  		barTarget.ID(): s2,
   156  	}
   157  
   158  	ref := container.MustParseNamedTagged(
   159  		store.LocalImageRefFromBuildResult(s1.LastResult))
   160  	f.setMissingImage(ref)
   161  
   162  	f.run(targets, buildStateSet)
   163  
   164  	fooCall := newFakeBuildHandlerCall(fooTarget, 1, []store.ImageBuildResult{})
   165  
   166  	fooRef := container.MustParseNamedTagged(
   167  		store.LocalImageRefFromBuildResult(fooCall.result))
   168  	barCall := newFakeBuildHandlerCall(barTarget, 2, []store.ImageBuildResult{
   169  		store.NewImageBuildResultSingleRef(fooTarget.ID(), fooRef),
   170  	})
   171  
   172  	// foo has a valid last result, but its image is missing, so we have to rebuild it and its deps
   173  	expectedCalls := map[model.TargetID]fakeBuildHandlerCall{
   174  		fooTarget.ID(): fooCall,
   175  		barTarget.ID(): barCall,
   176  	}
   177  	assert.Equal(t, expectedCalls, f.handler.calls)
   178  }
   179  
   180  func newFakeBuildHandlerCall(target model.ImageTarget, num int, depResults []store.ImageBuildResult) fakeBuildHandlerCall {
   181  	return fakeBuildHandlerCall{
   182  		target: target,
   183  		result: store.NewImageBuildResultSingleRef(
   184  			target.ID(),
   185  			container.MustParseNamedTagged(fmt.Sprintf("%s:%d", target.ImageMapSpec.Selector, num)),
   186  		),
   187  		depResults: depResults,
   188  	}
   189  }
   190  
   191  type fakeBuildHandlerCall struct {
   192  	target     model.TargetSpec
   193  	depResults []store.ImageBuildResult
   194  	result     store.ImageBuildResult
   195  }
   196  
   197  type fakeBuildHandler struct {
   198  	buildNum int
   199  	calls    map[model.TargetID]fakeBuildHandlerCall
   200  }
   201  
   202  func newFakeBuildHandler() *fakeBuildHandler {
   203  	return &fakeBuildHandler{
   204  		calls: make(map[model.TargetID]fakeBuildHandlerCall),
   205  	}
   206  }
   207  
   208  func (fbh *fakeBuildHandler) handle(target model.TargetSpec, depResults []store.ImageBuildResult) (store.ImageBuildResult, error) {
   209  	iTarget := target.(model.ImageTarget)
   210  	fbh.buildNum++
   211  	namedTagged := container.MustParseNamedTagged(fmt.Sprintf("%s:%d", iTarget.ImageMapSpec.Selector, fbh.buildNum))
   212  	result := store.NewImageBuildResultSingleRef(target.ID(), namedTagged)
   213  	fbh.calls[target.ID()] = fakeBuildHandlerCall{target, depResults, result}
   214  	return result, nil
   215  }
   216  
   217  type targetQueueFixture struct {
   218  	t             *testing.T
   219  	ctx           context.Context
   220  	handler       *fakeBuildHandler
   221  	missingImages []reference.NamedTagged
   222  }
   223  
   224  func newTargetQueueFixture(t *testing.T) *targetQueueFixture {
   225  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   226  	return &targetQueueFixture{
   227  		t:       t,
   228  		ctx:     ctx,
   229  		handler: newFakeBuildHandler(),
   230  	}
   231  }
   232  
   233  func (f *targetQueueFixture) imageExists(ctx context.Context, iTarget model.ImageTarget, namedTagged reference.NamedTagged) (b bool, e error) {
   234  	for _, ref := range f.missingImages {
   235  		if ref == namedTagged {
   236  			return false, nil
   237  		}
   238  	}
   239  	return true, nil
   240  }
   241  
   242  func (f *targetQueueFixture) setMissingImage(namedTagged reference.NamedTagged) {
   243  	f.missingImages = append(f.missingImages, namedTagged)
   244  }
   245  
   246  func (f *targetQueueFixture) run(targets []model.ImageTarget, buildStateSet store.BuildStateSet) {
   247  	tq, err := NewImageTargetQueue(f.ctx, targets, buildStateSet, f.imageExists)
   248  	if err != nil {
   249  		f.t.Fatal(err)
   250  	}
   251  
   252  	err = tq.RunBuilds(f.handler.handle)
   253  	if err != nil {
   254  		f.t.Fatal(err)
   255  	}
   256  }
   257  
   258  func newDockerImageTarget(ref string) model.ImageTarget {
   259  	return model.MustNewImageTarget(container.MustParseSelector(ref)).
   260  		WithBuildDetails(model.DockerBuild{})
   261  }