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  }