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 }