github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/scheduler_test.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package build 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "sync" 26 "sync/atomic" 27 "testing" 28 "time" 29 30 "github.com/google/go-cmp/cmp" 31 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 35 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/tag" 36 "github.com/GoogleContainerTools/skaffold/testutil" 37 testEvent "github.com/GoogleContainerTools/skaffold/testutil/event" 38 ) 39 40 func TestGetBuild(t *testing.T) { 41 tests := []struct { 42 description string 43 buildArtifact ArtifactBuilder 44 tags tag.ImageTags 45 expectedTag string 46 expectedOut string 47 shouldErr bool 48 }{ 49 { 50 description: "build succeeds", 51 buildArtifact: func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 52 out.Write([]byte("build succeeds")) 53 return fmt.Sprintf("%s@sha256:abac", tag), nil 54 }, 55 tags: tag.ImageTags{ 56 "skaffold/image1": "skaffold/image1:v0.0.1", 57 "skaffold/image2": "skaffold/image2:v0.0.2", 58 }, 59 expectedTag: "skaffold/image1:v0.0.1@sha256:abac", 60 expectedOut: "build succeeds", 61 }, 62 { 63 description: "tag with ko scheme prefix and Go import path with uppercase characters is sanitized", 64 buildArtifact: func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 65 out.Write([]byte("build succeeds")) 66 return fmt.Sprintf("%s@sha256:abac", tag), nil 67 }, 68 tags: tag.ImageTags{ 69 "skaffold/image1": "ko://github.com/GoogleContainerTools/skaffold/cmd/skaffold:v0.0.1", 70 }, 71 expectedTag: "github.com/googlecontainertools/skaffold/cmd/skaffold:v0.0.1@sha256:abac", 72 expectedOut: "build succeeds", 73 }, 74 { 75 description: "build fails", 76 buildArtifact: func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 77 return "", fmt.Errorf("build fails") 78 }, 79 tags: tag.ImageTags{ 80 "skaffold/image1": "", 81 }, 82 expectedOut: "", 83 shouldErr: true, 84 }, 85 { 86 description: "tag not found", 87 tags: tag.ImageTags{}, 88 expectedOut: "", 89 shouldErr: true, 90 }, 91 } 92 for _, test := range tests { 93 testutil.Run(t, test.description, func(t *testutil.T) { 94 out := new(bytes.Buffer) 95 96 artifact := &latest.Artifact{ImageName: "skaffold/image1"} 97 got, err := performBuild(context.Background(), out, test.tags, platform.Resolver{}, artifact, test.buildArtifact) 98 99 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expectedTag, got) 100 t.CheckDeepEqual(test.expectedOut, out.String()) 101 }) 102 } 103 } 104 105 func TestFormatResults(t *testing.T) { 106 tests := []struct { 107 description string 108 artifacts []*latest.Artifact 109 expected []graph.Artifact 110 results map[string]interface{} 111 shouldErr bool 112 }{ 113 { 114 description: "all builds completely successfully", 115 artifacts: []*latest.Artifact{ 116 {ImageName: "skaffold/image1"}, 117 {ImageName: "skaffold/image2"}, 118 }, 119 expected: []graph.Artifact{ 120 {ImageName: "skaffold/image1", Tag: "skaffold/image1:v0.0.1@sha256:abac"}, 121 {ImageName: "skaffold/image2", Tag: "skaffold/image2:v0.0.2@sha256:abac"}, 122 }, 123 results: map[string]interface{}{ 124 "skaffold/image1": "skaffold/image1:v0.0.1@sha256:abac", 125 "skaffold/image2": "skaffold/image2:v0.0.2@sha256:abac", 126 }, 127 }, 128 { 129 description: "no build result produced for a build", 130 artifacts: []*latest.Artifact{ 131 {ImageName: "skaffold/image1"}, 132 {ImageName: "skaffold/image2"}, 133 }, 134 expected: nil, 135 results: map[string]interface{}{ 136 "skaffold/image1": "skaffold/image1:v0.0.1@sha256:abac", 137 }, 138 shouldErr: true, 139 }, 140 } 141 for _, test := range tests { 142 testutil.Run(t, test.description, func(t *testutil.T) { 143 m := new(sync.Map) 144 for k, v := range test.results { 145 m.Store(k, v) 146 } 147 results := &artifactStoreImpl{m: m} 148 got, err := results.GetArtifacts(test.artifacts) 149 150 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, got) 151 }) 152 } 153 } 154 155 func TestInOrder(t *testing.T) { 156 tests := []struct { 157 description string 158 buildFunc ArtifactBuilder 159 expected string 160 }{ 161 { 162 description: "short and nice build log", 163 expected: "Building 2 artifacts in parallel\nBuilding [skaffold/image1]...\nshort\nBuild [skaffold/image1] succeeded\nBuilding [skaffold/image2]...\nshort\nBuild [skaffold/image2] succeeded\n", 164 buildFunc: func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 165 out.Write([]byte("short\n")) 166 return fmt.Sprintf("%s:tag", artifact.ImageName), nil 167 }, 168 }, 169 { 170 description: "long build log gets printed correctly", 171 expected: `Building 2 artifacts in parallel 172 Building [skaffold/image1]... 173 This is a long string more than 10 bytes. 174 And new lines 175 Build [skaffold/image1] succeeded 176 Building [skaffold/image2]... 177 This is a long string more than 10 bytes. 178 And new lines 179 Build [skaffold/image2] succeeded 180 `, 181 buildFunc: func(ctx context.Context, out io.Writer, artifact *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 182 out.Write([]byte("This is a long string more than 10 bytes.\nAnd new lines\n")) 183 return fmt.Sprintf("%s:tag", artifact.ImageName), nil 184 }, 185 }, 186 } 187 for _, test := range tests { 188 testutil.Run(t, test.description, func(t *testutil.T) { 189 out := new(bytes.Buffer) 190 artifacts := []*latest.Artifact{ 191 {ImageName: "skaffold/image1"}, 192 {ImageName: "skaffold/image2", Dependencies: []*latest.ArtifactDependency{{ImageName: "skaffold/image1"}}}, 193 } 194 tags := tag.ImageTags{ 195 "skaffold/image1": "skaffold/image1:v0.0.1", 196 "skaffold/image2": "skaffold/image2:v0.0.2", 197 } 198 initializeEvents() 199 200 InOrder(context.Background(), out, tags, platform.Resolver{}, artifacts, test.buildFunc, 0, NewArtifactStore()) 201 202 t.CheckDeepEqual(test.expected, out.String()) 203 }) 204 } 205 } 206 207 func TestInOrderConcurrency(t *testing.T) { 208 tests := []struct { 209 artifacts int 210 limit int 211 maxConcurrency int 212 }{ 213 { 214 artifacts: 10, 215 limit: 0, // default - no limit 216 maxConcurrency: 10, 217 }, 218 { 219 artifacts: 50, 220 limit: 1, 221 maxConcurrency: 1, 222 }, 223 { 224 artifacts: 50, 225 limit: 10, 226 maxConcurrency: 10, 227 }, 228 } 229 for _, test := range tests { 230 testutil.Run(t, fmt.Sprintf("%d artifacts, max concurrency=%d", test.artifacts, test.limit), func(t *testutil.T) { 231 var artifacts []*latest.Artifact 232 tags := tag.ImageTags{} 233 234 for i := 0; i < test.artifacts; i++ { 235 imageName := fmt.Sprintf("skaffold/image%d", i) 236 tag := fmt.Sprintf("skaffold/image%d:tag", i) 237 238 artifacts = append(artifacts, &latest.Artifact{ImageName: imageName}) 239 tags[imageName] = tag 240 } 241 242 var actualConcurrency int32 243 244 builder := func(_ context.Context, _ io.Writer, _ *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 245 if atomic.AddInt32(&actualConcurrency, 1) > int32(test.maxConcurrency) { 246 return "", fmt.Errorf("only %d build can run at a time", test.maxConcurrency) 247 } 248 time.Sleep(5 * time.Millisecond) 249 atomic.AddInt32(&actualConcurrency, -1) 250 251 return tag, nil 252 } 253 254 initializeEvents() 255 results, err := InOrder(context.Background(), ioutil.Discard, tags, platform.Resolver{}, artifacts, builder, test.limit, NewArtifactStore()) 256 257 t.CheckNoError(err) 258 t.CheckDeepEqual(test.artifacts, len(results)) 259 }) 260 } 261 } 262 263 func TestInOrderForArgs(t *testing.T) { 264 tests := []struct { 265 description string 266 buildArtifact ArtifactBuilder 267 artifactLen int 268 concurrency int 269 dependency map[int][]int 270 expected []graph.Artifact 271 err error 272 }{ 273 { 274 description: "runs in parallel for 2 artifacts with no dependency", 275 buildArtifact: func(_ context.Context, _ io.Writer, _ *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 276 return tag, nil 277 }, 278 artifactLen: 2, 279 expected: []graph.Artifact{ 280 {ImageName: "artifact1", Tag: "artifact1@tag1"}, 281 {ImageName: "artifact2", Tag: "artifact2@tag2"}, 282 }, 283 }, 284 { 285 description: "runs in parallel for 5 artifacts with dependencies", 286 buildArtifact: func(_ context.Context, _ io.Writer, _ *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 287 return tag, nil 288 }, 289 dependency: map[int][]int{ 290 0: {2, 3}, 291 1: {3}, 292 2: {1}, 293 3: {4}, 294 }, 295 artifactLen: 5, 296 expected: []graph.Artifact{ 297 {ImageName: "artifact1", Tag: "artifact1@tag1"}, 298 {ImageName: "artifact2", Tag: "artifact2@tag2"}, 299 {ImageName: "artifact3", Tag: "artifact3@tag3"}, 300 {ImageName: "artifact4", Tag: "artifact4@tag4"}, 301 {ImageName: "artifact5", Tag: "artifact5@tag5"}, 302 }, 303 }, 304 { 305 description: "runs with max concurrency of 2 for 5 artifacts with dependencies", 306 buildArtifact: func(_ context.Context, _ io.Writer, _ *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 307 return tag, nil 308 }, 309 dependency: map[int][]int{ 310 0: {2, 3}, 311 1: {3}, 312 2: {1}, 313 3: {4}, 314 }, 315 artifactLen: 5, 316 concurrency: 2, 317 expected: []graph.Artifact{ 318 {ImageName: "artifact1", Tag: "artifact1@tag1"}, 319 {ImageName: "artifact2", Tag: "artifact2@tag2"}, 320 {ImageName: "artifact3", Tag: "artifact3@tag3"}, 321 {ImageName: "artifact4", Tag: "artifact4@tag4"}, 322 {ImageName: "artifact5", Tag: "artifact5@tag5"}, 323 }, 324 }, 325 { 326 description: "runs in parallel should return for 0 artifacts", 327 artifactLen: 0, 328 expected: nil, 329 }, 330 { 331 description: "build fails for artifacts without dependencies", 332 buildArtifact: func(c context.Context, _ io.Writer, a *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 333 if a.ImageName == "artifact2" { 334 return "", fmt.Errorf(`some error occurred while building "artifact2"`) 335 } 336 select { 337 case <-c.Done(): 338 return "", c.Err() 339 case <-time.After(5 * time.Second): 340 return tag, nil 341 } 342 }, 343 artifactLen: 5, 344 expected: nil, 345 err: fmt.Errorf(`build [artifact2] failed: %w`, fmt.Errorf(`some error occurred while building "artifact2"`)), 346 }, 347 { 348 description: "build fails for artifacts with dependencies", 349 buildArtifact: func(_ context.Context, _ io.Writer, a *latest.Artifact, tag string, _ platform.Matcher) (string, error) { 350 if a.ImageName == "artifact2" { 351 return "", fmt.Errorf(`some error occurred while building "artifact2"`) 352 } 353 return tag, nil 354 }, 355 dependency: map[int][]int{ 356 0: {1}, 357 1: {2}, 358 2: {3}, 359 3: {4}, 360 }, 361 artifactLen: 5, 362 expected: nil, 363 err: fmt.Errorf(`build [artifact2] failed: %w`, fmt.Errorf(`some error occurred while building "artifact2"`)), 364 }, 365 } 366 for _, test := range tests { 367 testutil.Run(t, test.description, func(t *testutil.T) { 368 artifacts := make([]*latest.Artifact, test.artifactLen) 369 tags := tag.ImageTags{} 370 for i := 0; i < test.artifactLen; i++ { 371 a := fmt.Sprintf("artifact%d", i+1) 372 artifacts[i] = &latest.Artifact{ImageName: a} 373 tags[a] = fmt.Sprintf("%s@tag%d", a, i+1) 374 } 375 376 setDependencies(artifacts, test.dependency) 377 initializeEvents() 378 actual, err := InOrder(context.Background(), ioutil.Discard, tags, platform.Resolver{}, artifacts, test.buildArtifact, test.concurrency, NewArtifactStore()) 379 380 t.CheckDeepEqual(test.expected, actual) 381 t.CheckDeepEqual(test.err, err, cmp.Comparer(errorsComparer)) 382 }) 383 } 384 } 385 386 // setDependencies constructs a graph of artifact dependencies using the map as an adjacency list representation of indices in the artifacts array. 387 // For example: 388 // m = { 389 // 0 : {1, 2}, 390 // 2 : {3}, 391 //} 392 // implies that a[0] artifact depends on a[1] and a[2]; and a[2] depends on a[3]. 393 394 func setDependencies(a []*latest.Artifact, d map[int][]int) { 395 for k, dep := range d { 396 for i := range dep { 397 a[k].Dependencies = append(a[k].Dependencies, &latest.ArtifactDependency{ 398 ImageName: a[dep[i]].ImageName, 399 }) 400 } 401 } 402 } 403 404 func initializeEvents() { 405 pipes := []latest.Pipeline{{ 406 Deploy: latest.DeployConfig{}, 407 Build: latest.BuildConfig{ 408 BuildType: latest.BuildType{ 409 LocalBuild: &latest.LocalBuild{}, 410 }, 411 }, 412 }} 413 testEvent.InitializeState(pipes) 414 } 415 416 func errorsComparer(a, b error) bool { 417 if a == nil && b == nil { 418 return true 419 } 420 if a == nil || b == nil { 421 return false 422 } 423 return a.Error() == b.Error() 424 }