golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/buildrelease_test.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package relui 6 7 import ( 8 "archive/tar" 9 "archive/zip" 10 "bytes" 11 "compress/gzip" 12 "context" 13 "crypto/sha256" 14 "fmt" 15 "io" 16 "io/fs" 17 "net/http" 18 "net/http/httptest" 19 "os" 20 "path/filepath" 21 "runtime" 22 "strings" 23 "sync" 24 "sync/atomic" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 "github.com/google/go-github/github" 31 "github.com/google/uuid" 32 "github.com/shurcooL/githubv4" 33 "golang.org/x/build/gerrit" 34 "golang.org/x/build/internal" 35 "golang.org/x/build/internal/gcsfs" 36 "golang.org/x/build/internal/releasetargets" 37 "golang.org/x/build/internal/task" 38 "golang.org/x/build/internal/workflow" 39 ) 40 41 func TestRelease(t *testing.T) { 42 t.Run("minor", func(t *testing.T) { 43 testRelease(t, "go1.22", 22, "go1.22.1", task.KindMinor) 44 }) 45 t.Run("beta", func(t *testing.T) { 46 testRelease(t, "go1.22", 23, "go1.23beta1", task.KindBeta) 47 }) 48 t.Run("rc", func(t *testing.T) { 49 testRelease(t, "go1.22", 23, "go1.23rc1", task.KindRC) 50 }) 51 t.Run("major", func(t *testing.T) { 52 testRelease(t, "go1.22", 23, "go1.23.0", task.KindMajor) 53 }) 54 } 55 56 func TestSecurity(t *testing.T) { 57 t.Run("success", func(t *testing.T) { 58 testSecurity(t, true) 59 }) 60 t.Run("failure", func(t *testing.T) { 61 testSecurity(t, false) 62 }) 63 } 64 65 const fakeGo = `#!/bin/bash -eu 66 67 case "$1" in 68 "get") 69 ls go.mod go.sum >/dev/null 70 for i in "${@:2}"; do 71 echo -e "// pretend we've upgraded to $i" >> go.mod 72 echo "$i h1:asdasd" | tr '@' ' ' >> go.sum 73 done 74 ;; 75 "mod") 76 ls go.mod go.sum >/dev/null 77 echo "tidied!" >> go.mod 78 ;; 79 "generate") 80 mkdir -p internal/stdlib 81 cd internal/stdlib && echo "package stdlib" >> manifest.go 82 ;; 83 *) 84 echo unexpected command $@ 85 exit 1 86 ;; 87 esac 88 ` 89 90 type releaseTestDeps struct { 91 ctx context.Context 92 cancel context.CancelFunc 93 buildBucket *task.FakeBuildBucketClient 94 goRepo *task.FakeRepo 95 gerrit *reviewerCheckGerrit 96 versionTasks *task.VersionTasks 97 buildTasks *BuildReleaseTasks 98 milestoneTasks *task.MilestoneTasks 99 publishedFiles map[string]task.WebsiteFile 100 } 101 102 func newReleaseTestDeps(t *testing.T, previousTag string, major int, wantVersion string) *releaseTestDeps { 103 if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { 104 t.Skip("Requires bash shell scripting support.") 105 } 106 task.AwaitDivisor, workflow.MaxRetries = 100, 1 107 t.Cleanup(func() { task.AwaitDivisor, workflow.MaxRetries = 1, 3 }) 108 ctx, cancel := context.WithCancel(context.Background()) 109 110 // Set up a server that will be used to serve inputs to the build. 111 bootstrapServer := httptest.NewServer(http.HandlerFunc(serveBootstrap)) 112 t.Cleanup(bootstrapServer.Close) 113 114 // Set up the fake CDN publishing process. 115 servingDir := t.TempDir() 116 dlDir := t.TempDir() 117 dlServer := httptest.NewServer(http.FileServer(http.FS(os.DirFS(dlDir)))) 118 t.Cleanup(dlServer.Close) 119 go fakeCDNLoad(ctx, t, servingDir, dlDir) 120 121 // Set up the fake website to publish to. 122 var filesMu sync.Mutex 123 files := map[string]task.WebsiteFile{} 124 publishFile := func(f task.WebsiteFile) error { 125 filesMu.Lock() 126 defer filesMu.Unlock() 127 files[strings.TrimPrefix(f.Filename, wantVersion+".")] = f 128 return nil 129 } 130 131 goRepo := task.NewFakeRepo(t, "go") 132 base := goRepo.Commit(goFiles) 133 goRepo.Tag(previousTag, base) 134 goRepo.Branch(fmt.Sprintf("release-branch.go1.%d", major), base) 135 dlRepo := task.NewFakeRepo(t, "dl") 136 toolsRepo := task.NewFakeRepo(t, "tools") 137 toolsRepo1 := toolsRepo.Commit(map[string]string{ 138 "go.mod": "module golang.org/x/tools\n", 139 "go.sum": "\n", 140 "internal/imports/mkstdlib.go": "package imports\nconst C=1", 141 }) 142 toolsRepo.Tag("master", toolsRepo1) 143 fakeGerrit := task.NewFakeGerrit(t, goRepo, dlRepo, toolsRepo) 144 145 gerrit := &reviewerCheckGerrit{FakeGerrit: fakeGerrit} 146 versionTasks := &task.VersionTasks{ 147 Gerrit: gerrit, 148 CloudBuild: task.NewFakeCloudBuild(t, fakeGerrit, "", nil, fakeGo), 149 GoProject: "go", 150 } 151 milestoneTasks := &task.MilestoneTasks{ 152 Client: fakeGitHub{}, 153 RepoOwner: "golang", 154 RepoName: "go", 155 ApproveAction: func(ctx *workflow.TaskContext) error { 156 return fmt.Errorf("unexpected approval request for %q", ctx.TaskName) 157 }, 158 } 159 buildBucket := task.NewFakeBuildBucketClient(major, fakeGerrit.GerritURL(), "security-try", []string{"go"}) 160 161 const dockerProject, dockerTrigger = "docker-build-project", "docker-build-trigger" 162 163 scratchDir := t.TempDir() 164 165 buildTasks := &BuildReleaseTasks{ 166 GerritClient: gerrit, 167 GerritProject: "go", 168 GerritHTTPClient: http.DefaultClient, 169 GCSClient: nil, 170 ScratchFS: &task.ScratchFS{BaseURL: "file://" + scratchDir}, 171 SignedURL: "file://" + scratchDir + "/signed/outputs", 172 ServingURL: "file://" + filepath.ToSlash(servingDir), 173 SignService: task.NewFakeSignService(t, scratchDir+"/signed/outputs"), 174 DownloadURL: dlServer.URL, 175 ProxyPrefix: dlServer.URL, 176 PublishFile: publishFile, 177 GoogleDockerBuildProject: dockerProject, 178 GoogleDockerBuildTrigger: dockerTrigger, 179 BuildBucketClient: buildBucket, 180 CloudBuildClient: task.NewFakeCloudBuild(t, fakeGerrit, dockerProject, map[string]map[string]string{dockerTrigger: {"_GO_VERSION": wantVersion[2:]}}, ""), 181 SwarmingClient: task.NewFakeSwarmingClient(t, fakeGo), 182 ApproveAction: func(ctx *workflow.TaskContext) error { 183 if strings.Contains(ctx.TaskName, "Release Coordinator Approval") { 184 return nil 185 } 186 return fmt.Errorf("unexpected approval request for %q", ctx.TaskName) 187 }, 188 } 189 // Cleanups are called in reverse order, and we need to cancel the context 190 // before the temp dirs are deleted. 191 t.Cleanup(cancel) 192 return &releaseTestDeps{ 193 ctx: ctx, 194 cancel: cancel, 195 buildBucket: buildBucket, 196 goRepo: goRepo, 197 gerrit: gerrit, 198 versionTasks: versionTasks, 199 buildTasks: buildTasks, 200 milestoneTasks: milestoneTasks, 201 publishedFiles: files, 202 } 203 } 204 205 func testRelease(t *testing.T, prevTag string, major int, wantVersion string, kind task.ReleaseKind) { 206 deps := newReleaseTestDeps(t, prevTag, major, wantVersion) 207 wd := workflow.New() 208 209 deps.gerrit.wantReviewers = []string{"heschi", "dmitshur"} 210 v := addSingleReleaseWorkflow(deps.buildTasks, deps.milestoneTasks, deps.versionTasks, wd, major, kind, workflow.Const(deps.gerrit.wantReviewers)) 211 workflow.Output(wd, "Published Go version", v) 212 213 w, err := workflow.Start(wd, map[string]interface{}{ 214 "Targets to skip testing (or 'all') (optional)": []string{ 215 // allScript is intentionally hardcoded to fail on GOOS=js 216 // and we confirm here that it's possible to skip that. 217 "js-wasm-node18", // Builder used on 1.21 and newer. 218 "js-wasm", // Builder used on 1.20 and older. 219 }, 220 "Ref from the private repository to build from (optional)": "", 221 "Security repository to retrieve ref from (optional)": "", 222 }) 223 if err != nil { 224 t.Fatal(err) 225 } 226 outputs, err := w.Run(deps.ctx, &verboseListener{t: t, onStall: deps.cancel}) 227 if err != nil { 228 t.Fatal(err) 229 } 230 231 // Create a complete list of expected published files. 232 wantPublishedFiles := map[string]string{ 233 wantVersion + ".src.tar.gz": "source", 234 } 235 for _, t := range releasetargets.TargetsForGo1Point(major) { 236 switch t.GOOS { 237 case "darwin": 238 wantPublishedFiles[wantVersion+"."+t.Name+".tar.gz"] = "archive" 239 wantPublishedFiles[wantVersion+"."+t.Name+".pkg"] = "installer" 240 case "windows": 241 wantPublishedFiles[wantVersion+"."+t.Name+".zip"] = "archive" 242 wantPublishedFiles[wantVersion+"."+t.Name+".msi"] = "installer" 243 default: 244 wantPublishedFiles[wantVersion+"."+t.Name+".tar.gz"] = "archive" 245 } 246 } 247 248 dlURL, files := deps.buildTasks.DownloadURL, deps.publishedFiles 249 for _, f := range deps.publishedFiles { 250 wantKind, ok := wantPublishedFiles[f.Filename] 251 if !ok { 252 t.Errorf("got unexpected published file %q", f.Filename) 253 } else if got, want := f.Kind, wantKind; got != want { 254 t.Errorf("file %s has unexpected kind: got %q, want %q", f.Filename, got, want) 255 } 256 delete(wantPublishedFiles, f.Filename) 257 258 checkFile(t, dlURL, files, strings.TrimPrefix(f.Filename, wantVersion+"."), f, func(t *testing.T, b []byte) { 259 if got, want := len(b), int(f.Size); got != want { 260 t.Errorf("%s size mismatch with metadata: %v != %v", f.Filename, got, want) 261 } 262 if got, want := fmt.Sprintf("%x", sha256.Sum256(b)), f.ChecksumSHA256; got != want { 263 t.Errorf("%s sha256 mismatch with metadata: %q != %q", f.Filename, got, want) 264 } 265 if got, want := fmt.Sprintf("%x", sha256.Sum256(b)), string(fetch(t, dlURL+"/"+f.Filename+".sha256")); got != want { 266 t.Errorf("%s sha256 mismatch with .sha256 file: %q != %q", f.Filename, got, want) 267 } 268 if strings.HasSuffix(f.Filename, ".tar.gz") { 269 if got, want := string(fetch(t, dlURL+"/"+f.Filename+".asc")), fmt.Sprintf("I'm a GPG signature for %x!", sha256.Sum256(b)); got != want { 270 t.Errorf("%v doesn't have the expected GPG signature: got %s, want %s", f.Filename, got, want) 271 } 272 } 273 }) 274 } 275 if len(wantPublishedFiles) != 0 { 276 t.Errorf("missing %d published files: %v", len(wantPublishedFiles), wantPublishedFiles) 277 } 278 versionFile := outputs["VERSION file"].(string) 279 if !strings.Contains(versionFile, wantVersion) { 280 t.Errorf("version file should contain %q, got %q", wantVersion, versionFile) 281 } 282 checkTGZ(t, dlURL, files, "src.tar.gz", task.WebsiteFile{ 283 OS: "", 284 Arch: "", 285 Kind: "source", 286 }, map[string]string{ 287 "go/VERSION": versionFile, 288 "go/src/make.bash": makeScript, 289 }) 290 checkContents(t, dlURL, files, "windows-amd64.msi", task.WebsiteFile{ 291 OS: "windows", 292 Arch: "amd64", 293 Kind: "installer", 294 }, "I'm an MSI!\n-signed <Windows>") 295 checkTGZ(t, dlURL, files, "linux-amd64.tar.gz", task.WebsiteFile{ 296 OS: "linux", 297 Arch: "amd64", 298 Kind: "archive", 299 }, map[string]string{ 300 "go/VERSION": versionFile, 301 "go/tool/something_orother/compile": "", 302 }) 303 checkZip(t, dlURL, files, "windows-amd64.zip", task.WebsiteFile{ 304 OS: "windows", 305 Arch: "amd64", 306 Kind: "archive", 307 }, map[string]string{ 308 "go/VERSION": versionFile, 309 "go/tool/something_orother/compile": "", 310 }) 311 checkTGZ(t, dlURL, files, "linux-armv6l.tar.gz", task.WebsiteFile{ 312 OS: "linux", 313 Arch: "armv6l", 314 Kind: "archive", 315 }, map[string]string{ 316 "go/VERSION": versionFile, 317 "go/tool/something_orother/compile": "", 318 }) 319 checkTGZ(t, dlURL, files, "darwin-amd64.tar.gz", task.WebsiteFile{ 320 OS: "darwin", 321 Arch: "amd64", 322 Kind: "archive", 323 }, map[string]string{ 324 "go/VERSION": versionFile, 325 "go/bin/go": "-signed <macOS>", 326 }) 327 checkContents(t, dlURL, files, "darwin-amd64.pkg", task.WebsiteFile{ 328 OS: "darwin", 329 Arch: "amd64", 330 Kind: "installer", 331 }, "I'm a PKG! -signed <macOS>") 332 modVer := "v0.0.1-" + wantVersion + ".darwin-amd64" 333 checkContents(t, dlURL, nil, modVer+".mod", task.WebsiteFile{}, "module golang.org/toolchain") 334 checkContents(t, dlURL, nil, modVer+".info", task.WebsiteFile{}, fmt.Sprintf(`"Version":"%v"`, modVer)) 335 checkZip(t, dlURL, nil, modVer+".zip", task.WebsiteFile{}, map[string]string{ 336 "golang.org/toolchain@" + modVer + "/bin/go": "-signed <macOS>", 337 }) 338 339 head, err := deps.gerrit.ReadBranchHead(deps.ctx, "dl", "master") 340 if err != nil { 341 t.Fatal(err) 342 } 343 content, err := deps.gerrit.ReadFile(deps.ctx, "dl", head, wantVersion+"/main.go") 344 if err != nil { 345 t.Fatal(err) 346 } 347 if !strings.Contains(string(content), fmt.Sprintf("version.Run(%q)", wantVersion)) { 348 t.Errorf("unexpected dl content: %v", content) 349 } 350 351 tag, err := deps.gerrit.GetTag(deps.ctx, "go", wantVersion) 352 if err != nil { 353 t.Fatal(err) 354 } 355 356 if kind != task.KindBeta { 357 version, err := deps.gerrit.ReadFile(deps.ctx, "go", tag.Revision, "VERSION") 358 if err != nil { 359 t.Fatal(err) 360 } 361 if string(version) != versionFile { 362 t.Errorf("VERSION file is %q, expected %q", version, versionFile) 363 } 364 } 365 } 366 367 func testSecurity(t *testing.T, mergeFixes bool) { 368 deps := newReleaseTestDeps(t, "go1.17", 18, "go1.18rc1") 369 370 // Set up the fake merge process. Once we stop to ask for approval, commit 371 // the fix to the public server. 372 privateRepo := task.NewFakeRepo(t, "go") 373 privateRepo.Commit(goFiles) 374 securityFix := map[string]string{"security.txt": "This file makes us secure"} 375 privateRepoName := "go-internal/go (new)" 376 privateRef := privateRepo.Commit(securityFix) 377 privateGerrit := task.NewFakeGerrit(t, privateRepo) 378 deps.buildBucket.GerritURL = privateGerrit.GerritURL() 379 deps.buildBucket.Projects = []string{"go"} 380 deps.buildTasks.PrivateGerritClient = privateGerrit 381 382 defaultApprove := deps.buildTasks.ApproveAction 383 deps.buildTasks.ApproveAction = func(tc *workflow.TaskContext) error { 384 if mergeFixes { 385 deps.goRepo.CommitOnBranch("release-branch.go1.18", securityFix) 386 } 387 return defaultApprove(tc) 388 } 389 390 // Run the release. 391 wd := workflow.New() 392 v := addSingleReleaseWorkflow(deps.buildTasks, deps.milestoneTasks, deps.versionTasks, wd, 18, task.KindRC, workflow.Slice[string]()) 393 workflow.Output(wd, "Published Go version", v) 394 395 w, err := workflow.Start(wd, map[string]interface{}{ 396 "Targets to skip testing (or 'all') (optional)": []string{"js-wasm"}, 397 "Ref from the private repository to build from (optional)": privateRef, 398 "Security repository to retrieve ref from (optional)": privateRepoName, 399 }) 400 if err != nil { 401 t.Fatal(err) 402 } 403 404 if mergeFixes { 405 _, err = w.Run(deps.ctx, &verboseListener{t: t}) 406 if err != nil { 407 t.Fatal(err) 408 } 409 } else { 410 runToFailure(t, deps.ctx, w, "Check branch state matches source archive", &verboseListener{t: t}) 411 return 412 } 413 checkTGZ(t, deps.buildTasks.DownloadURL, deps.publishedFiles, "src.tar.gz", task.WebsiteFile{ 414 OS: "", 415 Arch: "", 416 Kind: "source", 417 }, map[string]string{ 418 "go/security.txt": "This file makes us secure", 419 }) 420 } 421 422 func TestAdvisoryTestsFail(t *testing.T) { 423 deps := newReleaseTestDeps(t, "go1.17", 18, "go1.18rc1") 424 deps.buildBucket.FailBuilds = append(deps.buildBucket.FailBuilds, "linux-amd64-longtest") 425 defaultApprove := deps.buildTasks.ApproveAction 426 var testApprovals atomic.Int32 427 deps.buildTasks.ApproveAction = func(ctx *workflow.TaskContext) error { 428 if strings.Contains(ctx.TaskName, "Run advisory") { 429 testApprovals.Add(1) 430 return nil 431 } 432 return defaultApprove(ctx) 433 } 434 435 // Run the release. 436 wd := workflow.New() 437 v := addSingleReleaseWorkflow(deps.buildTasks, deps.milestoneTasks, deps.versionTasks, wd, 18, task.KindRC, workflow.Slice[string]()) 438 workflow.Output(wd, "Published Go version", v) 439 440 w, err := workflow.Start(wd, map[string]interface{}{ 441 "Targets to skip testing (or 'all') (optional)": []string(nil), 442 "Ref from the private repository to build from (optional)": "", 443 "Security repository to retrieve ref from (optional)": "", 444 }) 445 if err != nil { 446 t.Fatal(err) 447 } 448 if _, err := w.Run(deps.ctx, &verboseListener{t: t}); err != nil { 449 t.Fatal(err) 450 } 451 if testApprovals.Load() != 1 { 452 t.Errorf("failed advisory builder didn't need approval") 453 } 454 } 455 456 // makeScript pretends to be make.bash. It creates a fake go command that 457 // knows how to fake the commands the release process runs. 458 const makeScript = `#!/bin/bash -eu 459 460 GO=../ 461 VERSION=$(head -n 1 $GO/VERSION) 462 463 if [[ $# >0 && $1 == "-distpack" ]]; then 464 mkdir -p $GO/pkg/distpack 465 tmp=$(mktemp $TMPDIR/buildrel.XXXXXXXX).tar 466 (cd $GO/.. && find . | xargs touch -t 202301010000 && find . | xargs chmod 0777 && tar cf $tmp go) 467 # On macOS, tar -czf puts a timestamp in the gzip header. Do it ourselves with --no-name to suppress it. 468 gzip --no-name $tmp 469 mv $tmp.gz $GO/pkg/distpack/$VERSION.src.tar.gz 470 fi 471 472 mkdir -p $GO/bin 473 474 cat <<'EOF' >$GO/bin/go 475 #!/bin/bash -eu 476 case "$@" in 477 "install -race") 478 # Installing the race mode stdlib. Doesn't matter where it's run. 479 mkdir -p $(dirname $0)/../pkg/something_orother/ 480 touch $(dirname $0)/../pkg/something_orother/race.a 481 ;; 482 "tool dist test -compile-only") 483 # Testing with -compile-only flag set. 484 exit 0 485 ;; 486 *) 487 echo "unexpected command $@" 488 exit 1 489 ;; 490 esac 491 EOF 492 chmod 0755 $GO/bin/go 493 494 # We don't know what GOOS_GOARCH we're "building" for, write some junk for 495 # versimilitude. 496 mkdir -p $GO/tool/something_orother/ 497 touch $GO/tool/something_orother/compile 498 499 if [[ $# >0 && $1 == "-distpack" ]]; then 500 case $GOOS in 501 "windows") 502 tmp=$(mktemp $TMPDIR/buildrel.XXXXXXXX).zip 503 # The zip command isn't installed on our buildlets. Python is. 504 (cd $GO/.. && find . | xargs touch -t 202301010000 && find . | xargs chmod 0777 && python3 -m zipfile -c $tmp go/) 505 mv $tmp $GO/pkg/distpack/$VERSION-$GOOS-$GOARCH.zip 506 ;; 507 *) 508 tmp=$(mktemp $TMPDIR/buildrel.XXXXXXXX).tar 509 (cd $GO/.. && find . | xargs touch -t 202301010000 && find . | xargs chmod 0777 && tar cf $tmp go) 510 # On macOS, tar -czf puts a timestamp in the gzip header. Do it ourselves with --no-name to suppress it. 511 gzip --no-name $tmp 512 mv $tmp.gz $GO/pkg/distpack/$VERSION-$GOOS-$GOARCH.tar.gz 513 ;; 514 esac 515 516 MODVER=v0.0.1-$VERSION.$GOOS-$GOARCH 517 echo "module golang.org/toolchain" > $GO/pkg/distpack/$MODVER.mod 518 echo -e "{\"Version\":\"$MODVER\", \"Timestamp\":\"fake timestamp\"}" > $GO/pkg/distpack/$MODVER.info 519 MODTMP=$(mktemp -d $TMPDIR/buildrel.XXXXXXXX) 520 MODDIR=$MODTMP/golang.org/toolchain@$MODVER 521 mkdir -p $MODDIR 522 cp -r $GO $MODDIR 523 tmp=$(mktemp -d $TMPDIR/buildrel.XXXXXXXX).zip 524 (cd $MODTMP && find . | xargs touch -t 202301010000 && find . | xargs chmod 0777 && python3 -m zipfile -c $tmp .) 525 mv $tmp $GO/pkg/distpack/$MODVER.zip 526 fi 527 ` 528 529 // allScript pretends to be all.bash. It's hardcoded 530 // to fail on GOOS=js and pass on all other builders. 531 const allScript = `#!/bin/bash -eu 532 533 echo "I'm a test! :D" 534 535 if [[ ${GOOS:-} = "js" ]]; then 536 echo "Oh no, JavaScript is broken." 537 exit 1 538 fi 539 540 exit 0 541 ` 542 543 // raceScript pretends to be race.bash. 544 const raceScript = `#!/bin/bash -eu 545 546 echo "I'm a race test. Zoom zoom!" 547 548 exit 0 549 ` 550 551 var goFiles = map[string]string{ 552 "src/make.bash": makeScript, 553 "src/make.bat": makeScript, 554 "src/all.bash": allScript, 555 "src/all.bat": allScript, 556 "src/race.bash": raceScript, 557 "src/race.bat": raceScript, 558 } 559 560 func serveBootstrap(w http.ResponseWriter, r *http.Request) { 561 task.ServeTarball("go-builder-data/go", map[string]string{ 562 "bin/go": fakeGo, 563 }, w, r) 564 } 565 566 func checkFile(t *testing.T, dlURL string, files map[string]task.WebsiteFile, filename string, meta task.WebsiteFile, check func(*testing.T, []byte)) { 567 t.Run(filename, func(t *testing.T) { 568 resolvedName := filename 569 if files != nil { 570 f, ok := files[filename] 571 if !ok { 572 t.Fatalf("file %q not published", filename) 573 } 574 if diff := cmp.Diff(meta, f, cmpopts.IgnoreFields(task.WebsiteFile{}, "Filename", "Version", "ChecksumSHA256", "Size")); diff != "" { 575 t.Errorf("file metadata mismatch (-want +got):\n%v", diff) 576 } 577 resolvedName = f.Filename 578 } 579 body := fetch(t, dlURL+"/"+resolvedName) 580 check(t, body) 581 }) 582 } 583 func fetch(t *testing.T, url string) []byte { 584 resp, err := http.Get(url) 585 if err != nil { 586 t.Fatalf("getting %v: %v", url, err) 587 } 588 defer resp.Body.Close() 589 if resp.StatusCode != http.StatusOK { 590 t.Fatalf("getting %v: non-200 OK status code %v", url, resp.Status) 591 } 592 b, err := io.ReadAll(resp.Body) 593 if err != nil { 594 t.Fatalf("reading %v: %v", url, err) 595 } 596 return b 597 } 598 599 func checkContents(t *testing.T, dlURL string, files map[string]task.WebsiteFile, filename string, meta task.WebsiteFile, contents string) { 600 checkFile(t, dlURL, files, filename, meta, func(t *testing.T, b []byte) { 601 if got, want := string(b), contents; !strings.Contains(got, want) { 602 t.Errorf("%v contains %q, want %q", filename, got, want) 603 } 604 }) 605 } 606 607 func checkTGZ(t *testing.T, dlURL string, files map[string]task.WebsiteFile, filename string, meta task.WebsiteFile, contents map[string]string) { 608 checkFile(t, dlURL, files, filename, meta, func(t *testing.T, b []byte) { 609 gzr, err := gzip.NewReader(bytes.NewReader(b)) 610 if err != nil { 611 t.Fatal(err) 612 } 613 tr := tar.NewReader(gzr) 614 for { 615 h, err := tr.Next() 616 if err == io.EOF { 617 break 618 } 619 if err != nil { 620 t.Fatal(err) 621 } 622 want, ok := contents[h.Name] 623 if !ok { 624 continue 625 } 626 b, err := io.ReadAll(tr) 627 if err != nil { 628 t.Fatal(err) 629 } 630 delete(contents, h.Name) 631 if got := string(b); !strings.Contains(got, want) { 632 t.Errorf("%v contains %q, want %q", filename, got, want) 633 } 634 } 635 if len(contents) != 0 { 636 t.Errorf("not all files were found: missing %v", contents) 637 } 638 }) 639 } 640 641 func checkZip(t *testing.T, dlURL string, files map[string]task.WebsiteFile, filename string, meta task.WebsiteFile, contents map[string]string) { 642 checkFile(t, dlURL, files, filename, meta, func(t *testing.T, b []byte) { 643 zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) 644 if err != nil { 645 t.Fatal(err) 646 } 647 for _, f := range zr.File { 648 want, ok := contents[f.Name] 649 if !ok { 650 continue 651 } 652 r, err := zr.Open(f.Name) 653 if err != nil { 654 t.Fatal(err) 655 } 656 b, err := io.ReadAll(r) 657 if err != nil { 658 t.Fatal(err) 659 } 660 delete(contents, f.Name) 661 if got := string(b); !strings.Contains(got, want) { 662 t.Errorf("%v contains %q, want %q", filename, got, want) 663 } 664 } 665 if len(contents) != 0 { 666 t.Errorf("not all files were found: missing %v", contents) 667 } 668 }) 669 } 670 671 type reviewerCheckGerrit struct { 672 wantReviewers []string 673 *task.FakeGerrit 674 } 675 676 func (g *reviewerCheckGerrit) CreateAutoSubmitChange(ctx *workflow.TaskContext, input gerrit.ChangeInput, reviewers []string, contents map[string]string) (string, error) { 677 if diff := cmp.Diff(g.wantReviewers, reviewers, cmpopts.EquateEmpty()); diff != "" { 678 return "", fmt.Errorf("unexpected reviewers for CL: %v", diff) 679 } 680 return g.FakeGerrit.CreateAutoSubmitChange(ctx, input, reviewers, contents) 681 } 682 683 type fakeGitHub struct{} 684 685 func (fakeGitHub) FetchMilestone(_ context.Context, owner, repo, name string, create bool) (int, error) { 686 return 0, nil 687 } 688 689 func (fakeGitHub) FetchMilestoneIssues(_ context.Context, owner, repo string, milestoneID int) (map[int]map[string]bool, error) { 690 return nil, nil 691 } 692 693 func (fakeGitHub) EditIssue(_ context.Context, owner string, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error) { 694 return nil, nil, nil 695 } 696 697 func (fakeGitHub) EditMilestone(_ context.Context, owner string, repo string, number int, milestone *github.Milestone) (*github.Milestone, *github.Response, error) { 698 return nil, nil, nil 699 } 700 701 func (fakeGitHub) PostComment(_ context.Context, _ githubv4.ID, _ string) error { 702 return fmt.Errorf("pretend that PostComment failed") 703 } 704 705 type verboseListener struct { 706 t *testing.T 707 onStall func() 708 } 709 710 func (l *verboseListener) WorkflowStalled(workflowID uuid.UUID) error { 711 l.t.Logf("workflow %q: stalled", workflowID.String()) 712 if l.onStall != nil { 713 l.onStall() 714 } 715 return nil 716 } 717 718 func (l *verboseListener) TaskStateChanged(_ uuid.UUID, _ string, st *workflow.TaskState) error { 719 switch { 720 case !st.Finished: 721 l.t.Logf("task %-10v: started", st.Name) 722 case st.Error != "": 723 l.t.Logf("task %-10v: error: %v", st.Name, st.Error) 724 default: 725 l.t.Logf("task %-10v: done: %v", st.Name, st.Result) 726 } 727 return nil 728 } 729 730 func (l *verboseListener) Logger(_ uuid.UUID, task string) workflow.Logger { 731 return &testLogger{t: l.t, task: task} 732 } 733 734 type testLogger struct { 735 t *testing.T 736 task string 737 } 738 739 func (l *testLogger) Printf(format string, v ...interface{}) { 740 if l.task == "linux-amd64: Run long tests" && fmt.Sprintf(format, v...) == "Creating buildlet linux-amd64-bullseye." { 741 // TODO: This is very brittle; replace with a better way to test this property hasn't regressed. 742 l.t.Errorf("task %q logged creation of a non-longtest buildlet", l.task) 743 } 744 l.t.Logf("task %-10v: LOG: %s", l.task, fmt.Sprintf(format, v...)) 745 } 746 747 func runToFailure(t *testing.T, ctx context.Context, w *workflow.Workflow, task string, wrap workflow.Listener) string { 748 ctx, cancel := context.WithCancel(ctx) 749 defer cancel() 750 t.Helper() 751 var message string 752 listener := &errorListener{ 753 taskName: task, 754 callback: func(m string) { 755 message = m 756 cancel() 757 }, 758 Listener: wrap, 759 } 760 _, err := w.Run(ctx, listener) 761 if err == nil { 762 t.Fatalf("workflow unexpectedly succeeded") 763 } 764 return message 765 } 766 767 type errorListener struct { 768 taskName string 769 callback func(string) 770 workflow.Listener 771 } 772 773 func (l *errorListener) TaskStateChanged(id uuid.UUID, taskID string, st *workflow.TaskState) error { 774 if st.Name == l.taskName && st.Finished && st.Error != "" { 775 l.callback(st.Error) 776 } 777 l.Listener.TaskStateChanged(id, taskID, st) 778 return nil 779 } 780 781 func fakeCDNLoad(ctx context.Context, t *testing.T, from, to string) { 782 fromFS, toFS := gcsfs.DirFS(from), gcsfs.DirFS(to) 783 seen := map[string]bool{} 784 periodicallyDo(ctx, t, 100*time.Millisecond, func() error { 785 files, err := fs.ReadDir(fromFS, ".") 786 if err != nil { 787 return err 788 } 789 for _, f := range files { 790 if seen[f.Name()] { 791 continue 792 } 793 seen[f.Name()] = true 794 contents, err := fs.ReadFile(fromFS, f.Name()) 795 if err != nil { 796 return err 797 } 798 if err := gcsfs.WriteFile(toFS, f.Name(), contents); err != nil { 799 return err 800 } 801 } 802 return nil 803 }) 804 } 805 806 func periodicallyDo(ctx context.Context, t *testing.T, period time.Duration, f func() error) { 807 var err error 808 childCtx, cancel := context.WithCancel(ctx) 809 internal.PeriodicallyDo(childCtx, period, func(_ context.Context, _ time.Time) { 810 err = f() 811 if err != nil { 812 cancel() 813 } 814 }) 815 // Suppress errors caused by the test finishing before we notice. 816 if err != nil && ctx.Err() == nil { 817 t.Fatal(err) 818 } 819 }