golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/fakes.go (about) 1 // Copyright 2023 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 task 6 7 import ( 8 "archive/tar" 9 "bytes" 10 "compress/gzip" 11 "context" 12 "crypto/sha256" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io" 17 "io/fs" 18 "math/rand" 19 "net/http" 20 "net/http/httptest" 21 "os" 22 "os/exec" 23 "path" 24 "path/filepath" 25 "reflect" 26 "regexp" 27 "strconv" 28 "strings" 29 "sync" 30 "testing" 31 "time" 32 33 "github.com/google/uuid" 34 pb "go.chromium.org/luci/buildbucket/proto" 35 "golang.org/x/build/gerrit" 36 "golang.org/x/build/internal/gcsfs" 37 "golang.org/x/build/internal/installer/darwinpkg" 38 "golang.org/x/build/internal/installer/windowsmsi" 39 "golang.org/x/build/internal/relui/sign" 40 wf "golang.org/x/build/internal/workflow" 41 "golang.org/x/exp/slices" 42 "google.golang.org/protobuf/types/known/structpb" 43 ) 44 45 // ServeTarball serves files as a .tar.gz to w, only if path contains pathMatch. 46 func ServeTarball(pathMatch string, files map[string]string, w http.ResponseWriter, r *http.Request) { 47 if !strings.Contains(r.URL.Path, pathMatch) { 48 w.WriteHeader(http.StatusNotFound) 49 return 50 } 51 tgz, err := mapToTgz(files) 52 if err != nil { 53 panic(err) 54 } 55 if _, err := w.Write(tgz); err != nil { 56 panic(err) 57 } 58 } 59 60 func mapToTgz(files map[string]string) ([]byte, error) { 61 w := &bytes.Buffer{} 62 gzw := gzip.NewWriter(w) 63 tw := tar.NewWriter(gzw) 64 65 for name, contents := range files { 66 if err := tw.WriteHeader(&tar.Header{ 67 Typeflag: tar.TypeReg, 68 Name: name, 69 Size: int64(len(contents)), 70 Mode: 0777, 71 ModTime: time.Now(), 72 AccessTime: time.Now(), 73 ChangeTime: time.Now(), 74 }); err != nil { 75 return nil, err 76 } 77 if _, err := tw.Write([]byte(contents)); err != nil { 78 return nil, err 79 } 80 } 81 82 if err := tw.Close(); err != nil { 83 return nil, err 84 } 85 if err := gzw.Close(); err != nil { 86 return nil, err 87 } 88 return w.Bytes(), nil 89 } 90 91 func NewFakeGerrit(t *testing.T, repos ...*FakeRepo) *FakeGerrit { 92 result := &FakeGerrit{ 93 repos: map[string]*FakeRepo{}, 94 } 95 server := httptest.NewServer(http.HandlerFunc(result.serveHTTP)) 96 result.serverURL = server.URL 97 t.Cleanup(server.Close) 98 99 for _, r := range repos { 100 result.repos[r.name] = r 101 } 102 return result 103 } 104 105 type FakeGerrit struct { 106 repos map[string]*FakeRepo 107 serverURL string 108 } 109 110 type FakeRepo struct { 111 t *testing.T 112 name string 113 dir *GitDir 114 } 115 116 func NewFakeRepo(t *testing.T, name string) *FakeRepo { 117 if _, err := exec.LookPath("git"); errors.Is(err, exec.ErrNotFound) { 118 t.Skip("test requires git") 119 } 120 121 tmpDir := t.TempDir() 122 repoDir := filepath.Join(tmpDir, name) 123 if err := os.Mkdir(repoDir, 0700); err != nil { 124 t.Fatalf("failed to create repository directory: %s", err) 125 } 126 r := &FakeRepo{ 127 t: t, 128 name: name, 129 dir: &GitDir{&Git{}, repoDir}, 130 } 131 t.Cleanup(func() { r.dir.Close() }) 132 r.runGit("init") 133 r.runGit("commit", "--allow-empty", "--allow-empty-message", "-m", "") 134 return r 135 } 136 137 // TODO(rfindley): probably every method on FakeRepo should invoke 138 // repo.t.Helper(), otherwise it's impossible to see where the test failed. 139 140 func (repo *FakeRepo) runGit(args ...string) []byte { 141 repo.t.Helper() 142 configArgs := []string{ 143 "-c", "init.defaultBranch=master", 144 "-c", "user.email=relui@example.com", 145 "-c", "user.name=relui", 146 } 147 out, err := repo.dir.RunCommand(context.Background(), append(configArgs, args...)...) 148 if err != nil { 149 repo.t.Fatalf("runGit(%v) failed: %v; output:\n%s", args, err, out) 150 } 151 return out 152 } 153 154 func (repo *FakeRepo) Commit(contents map[string]string) string { 155 return repo.CommitOnBranch("master", contents) 156 } 157 158 func (repo *FakeRepo) CommitOnBranch(branch string, contents map[string]string) string { 159 repo.runGit("switch", branch) 160 for k, v := range contents { 161 full := filepath.Join(repo.dir.dir, k) 162 if err := os.MkdirAll(filepath.Dir(full), 0777); err != nil { 163 repo.t.Fatal(err) 164 } 165 if err := os.WriteFile(full, []byte(v), 0777); err != nil { 166 repo.t.Fatal(err) 167 } 168 } 169 repo.runGit("add", ".") 170 repo.runGit("commit", "--allow-empty-message", "-m", "") 171 return strings.TrimSpace(string(repo.runGit("rev-parse", "HEAD"))) 172 } 173 174 func (repo *FakeRepo) History() []string { 175 return strings.Split(string(repo.runGit("log", "--format=%H")), "\n") 176 } 177 178 func (repo *FakeRepo) Tag(tag, commit string) { 179 repo.runGit("tag", tag, commit) 180 } 181 182 func (repo *FakeRepo) Branch(branch, commit string) { 183 repo.runGit("branch", branch, commit) 184 } 185 186 func (repo *FakeRepo) ReadFile(commit, file string) ([]byte, error) { 187 b, err := repo.dir.RunCommand(context.Background(), "show", commit+":"+file) 188 if err != nil && strings.Contains(err.Error(), " does not exist ") { 189 err = errors.Join(gerrit.ErrResourceNotExist, err) 190 } 191 return b, err 192 } 193 194 var _ GerritClient = (*FakeGerrit)(nil) 195 196 func (g *FakeGerrit) GitilesURL() string { 197 return g.serverURL 198 } 199 200 func (g *FakeGerrit) ListProjects(ctx context.Context) ([]string, error) { 201 var names []string 202 for k := range g.repos { 203 names = append(names, k) 204 } 205 return names, nil 206 } 207 208 func (g *FakeGerrit) repo(name string) (*FakeRepo, error) { 209 if r, ok := g.repos[name]; ok { 210 return r, nil 211 } else { 212 return nil, fmt.Errorf("no such repo %v: %w", name, gerrit.ErrResourceNotExist) 213 } 214 } 215 216 func (g *FakeGerrit) ReadBranchHead(ctx context.Context, project, branch string) (string, error) { 217 repo, err := g.repo(project) 218 if err != nil { 219 return "", err 220 } 221 // TODO: If the branch doesn't exist, return an error matching gerrit.ErrResourceNotExist. 222 out, err := repo.dir.RunCommand(ctx, "rev-parse", "refs/heads/"+branch) 223 return strings.TrimSpace(string(out)), err 224 } 225 226 func (g *FakeGerrit) ReadFile(ctx context.Context, project string, commit string, file string) ([]byte, error) { 227 repo, err := g.repo(project) 228 if err != nil { 229 return nil, err 230 } 231 return repo.ReadFile(commit, file) 232 } 233 234 func (g *FakeGerrit) ListTags(ctx context.Context, project string) ([]string, error) { 235 repo, err := g.repo(project) 236 if err != nil { 237 return nil, err 238 } 239 out, err := repo.dir.RunCommand(ctx, "tag", "-l") 240 if err != nil { 241 return nil, err 242 } 243 if len(out) == 0 { 244 return nil, nil // No tags. 245 } 246 return strings.Split(strings.TrimSpace(string(out)), "\n"), nil 247 } 248 249 func (g *FakeGerrit) GetTag(ctx context.Context, project string, tag string) (gerrit.TagInfo, error) { 250 repo, err := g.repo(project) 251 if err != nil { 252 return gerrit.TagInfo{}, err 253 } 254 out, err := repo.dir.RunCommand(ctx, "rev-parse", "refs/tags/"+tag) 255 return gerrit.TagInfo{Revision: strings.TrimSpace(string(out))}, err 256 } 257 258 func (g *FakeGerrit) CreateAutoSubmitChange(_ *wf.TaskContext, input gerrit.ChangeInput, reviewers []string, contents map[string]string) (string, error) { 259 repo, err := g.repo(input.Project) 260 if err != nil { 261 return "", err 262 } 263 commit := repo.CommitOnBranch(input.Branch, contents) 264 return "cl_" + commit, nil 265 } 266 267 func (g *FakeGerrit) Submitted(ctx context.Context, changeID, baseCommit string) (string, bool, error) { 268 return strings.TrimPrefix(changeID, "cl_"), true, nil 269 } 270 271 func (g *FakeGerrit) Tag(ctx context.Context, project, tag, commit string) error { 272 repo, err := g.repo(project) 273 if err != nil { 274 return err 275 } 276 repo.Tag(tag, commit) 277 return nil 278 } 279 280 func (g *FakeGerrit) GetCommitsInRefs(ctx context.Context, project string, commits, refs []string) (map[string][]string, error) { 281 repo, err := g.repo(project) 282 if err != nil { 283 return nil, err 284 } 285 refSet := map[string]bool{} 286 for _, ref := range refs { 287 refSet[ref] = true 288 } 289 290 result := map[string][]string{} 291 for _, commit := range commits { 292 out, err := repo.dir.RunCommand(ctx, "branch", "--format=%(refname)", "--contains="+commit) 293 if err != nil { 294 return nil, err 295 } 296 for _, branch := range strings.Split(strings.TrimSpace(string(out)), "\n") { 297 branch := strings.TrimSpace(branch) 298 if refSet[branch] { 299 result[commit] = append(result[commit], branch) 300 } 301 } 302 } 303 return result, nil 304 } 305 306 func (g *FakeGerrit) GerritURL() string { 307 return g.serverURL 308 } 309 310 func (g *FakeGerrit) serveHTTP(w http.ResponseWriter, r *http.Request) { 311 parts := strings.Split(r.URL.Path, "/") 312 if len(parts) != 4 { 313 w.WriteHeader(http.StatusNotFound) 314 return 315 } 316 repo, err := g.repo(parts[1]) 317 if err != nil { 318 w.WriteHeader(http.StatusNotFound) 319 return 320 } 321 rev := strings.TrimSuffix(parts[3], ".tar.gz") 322 archive, err := repo.dir.RunCommand(r.Context(), "archive", "--format=tgz", rev) 323 if err != nil { 324 w.WriteHeader(http.StatusInternalServerError) 325 return 326 } 327 http.ServeContent(w, r, parts[3], time.Now(), bytes.NewReader(archive)) 328 } 329 330 func (*FakeGerrit) QueryChanges(_ context.Context, query string) ([]*gerrit.ChangeInfo, error) { 331 return nil, nil 332 } 333 334 func (*FakeGerrit) SetHashtags(_ context.Context, changeID string, _ gerrit.HashtagsInput) error { 335 return fmt.Errorf("pretend that SetHashtags failed") 336 } 337 338 func (*FakeGerrit) GetChange(_ context.Context, _ string, _ ...gerrit.QueryChangesOpt) (*gerrit.ChangeInfo, error) { 339 return nil, nil 340 } 341 342 // NewFakeSignService returns a fake signing service that can sign PKGs, MSIs, 343 // and generate GPG signatures. MSIs are "signed" by adding a suffix to them. 344 // PKGs must actually be tarballs with a prefix of "I'm a PKG!\n". Any files 345 // they contain that look like binaries will be "signed". 346 func NewFakeSignService(t *testing.T, outputDir string) *FakeSignService { 347 return &FakeSignService{ 348 t: t, 349 outputDir: outputDir, 350 completedJobs: map[string][]string{}, 351 } 352 } 353 354 type FakeSignService struct { 355 t *testing.T 356 outputDir string 357 mu sync.Mutex 358 completedJobs map[string][]string // Job ID → output objectURIs. 359 } 360 361 func (s *FakeSignService) SignArtifact(_ context.Context, bt sign.BuildType, in []string) (jobID string, _ error) { 362 s.t.Logf("fakeSignService: doing %s signing of %q", bt, in) 363 jobID = uuid.NewString() 364 var out []string 365 switch bt { 366 case sign.BuildMacOSConstructInstallerOnly: 367 if len(in) != 2 { 368 return "", fmt.Errorf("got %d inputs, want 2", len(in)) 369 } 370 out = []string{s.fakeConstructPKG(jobID, in[0], in[1], fmt.Sprintf("-installer <%s>", bt))} 371 case sign.BuildWindowsConstructInstallerOnly: 372 if len(in) != 2 { 373 return "", fmt.Errorf("got %d inputs, want 2", len(in)) 374 } 375 out = []string{s.fakeConstructMSI(jobID, in[0], in[1], fmt.Sprintf("-installer <%s>", bt))} 376 377 case sign.BuildMacOS: 378 if len(in) != 1 { 379 return "", fmt.Errorf("got %d inputs, want 1", len(in)) 380 } 381 out = []string{s.fakeSignPKG(jobID, in[0], fmt.Sprintf("-signed <%s>", bt))} 382 case sign.BuildWindows: 383 if len(in) != 1 { 384 return "", fmt.Errorf("got %d inputs, want 1", len(in)) 385 } 386 out = []string{s.fakeSignFile(jobID, in[0], fmt.Sprintf("-signed <%s>", bt))} 387 case sign.BuildGPG: 388 if len(in) == 0 { 389 return "", fmt.Errorf("got 0 inputs, want 1 or more") 390 } 391 for _, f := range in { 392 out = append(out, s.fakeGPGFile(jobID, f)) 393 } 394 default: 395 return "", fmt.Errorf("SignArtifact: not implemented for %v", bt) 396 } 397 s.mu.Lock() 398 s.completedJobs[jobID] = out 399 s.mu.Unlock() 400 return jobID, nil 401 } 402 403 func (s *FakeSignService) ArtifactSigningStatus(_ context.Context, jobID string) (_ sign.Status, desc string, out []string, _ error) { 404 s.mu.Lock() 405 out, ok := s.completedJobs[jobID] 406 s.mu.Unlock() 407 if !ok { 408 return sign.StatusNotFound, fmt.Sprintf("job %q not found", jobID), nil, nil 409 } 410 return sign.StatusCompleted, "", out, nil 411 } 412 413 func (s *FakeSignService) CancelSigning(_ context.Context, jobID string) error { 414 s.t.Errorf("CancelSigning was called unexpectedly") 415 return fmt.Errorf("intentional fake error") 416 } 417 418 func (s *FakeSignService) fakeConstructPKG(jobID, f, meta, msg string) string { 419 // Check installer metadata. 420 b, err := os.ReadFile(strings.TrimPrefix(meta, "file://")) 421 if err != nil { 422 panic(fmt.Errorf("fakeConstructPKG: os.ReadFile: %v", err)) 423 } 424 var opt darwinpkg.InstallerOptions 425 if err := json.Unmarshal(b, &opt); err != nil { 426 panic(fmt.Errorf("fakeConstructPKG: json.Unmarshal: %v", err)) 427 } 428 var errs []error 429 switch opt.GOARCH { 430 case "amd64", "arm64": // OK. 431 default: 432 errs = append(errs, fmt.Errorf("unexpected GOARCH value: %q", opt.GOARCH)) 433 } 434 switch min, _ := strconv.Atoi(opt.MinMacOSVersion); { 435 case min >= 11: // macOS 11 or greater; OK. 436 case opt.MinMacOSVersion == "10.15": // OK. 437 case opt.MinMacOSVersion == "10.13": // OK. Go 1.20 has macOS 10.13 as its minimum. 438 default: 439 errs = append(errs, fmt.Errorf("unexpected MinMacOSVersion value: %q", opt.MinMacOSVersion)) 440 } 441 if err := errors.Join(errs...); err != nil { 442 panic(fmt.Errorf("fakeConstructPKG: unexpected installer options %#v: %v", opt, err)) 443 } 444 445 // Construct fake installer. 446 b, err = os.ReadFile(strings.TrimPrefix(f, "file://")) 447 if err != nil { 448 panic(fmt.Errorf("fakeConstructPKG: os.ReadFile: %v", err)) 449 } 450 return s.writeOutput(jobID, path.Base(f)+".pkg", append([]byte("I'm a PKG!\n"), b...)) 451 } 452 453 func (s *FakeSignService) fakeConstructMSI(jobID, f, meta, msg string) string { 454 // Check installer metadata. 455 b, err := os.ReadFile(strings.TrimPrefix(meta, "file://")) 456 if err != nil { 457 panic(fmt.Errorf("fakeConstructMSI: os.ReadFile: %v", err)) 458 } 459 var opt windowsmsi.InstallerOptions 460 if err := json.Unmarshal(b, &opt); err != nil { 461 panic(fmt.Errorf("fakeConstructMSI: json.Unmarshal: %v", err)) 462 } 463 var errs []error 464 switch opt.GOARCH { 465 case "386", "amd64", "arm", "arm64": // OK. 466 default: 467 errs = append(errs, fmt.Errorf("unexpected GOARCH value: %q", opt.GOARCH)) 468 } 469 if err := errors.Join(errs...); err != nil { 470 panic(fmt.Errorf("fakeConstructMSI: unexpected installer options %#v: %v", opt, err)) 471 } 472 473 // Construct fake installer. 474 _, err = os.ReadFile(strings.TrimPrefix(f, "file://")) 475 if err != nil { 476 panic(fmt.Errorf("fakeConstructMSI: os.ReadFile: %v", err)) 477 } 478 return s.writeOutput(jobID, path.Base(f)+".msi", []byte("I'm an MSI!\n")) 479 } 480 481 func (s *FakeSignService) fakeSignPKG(jobID, f, msg string) string { 482 b, err := os.ReadFile(strings.TrimPrefix(f, "file://")) 483 if err != nil { 484 panic(fmt.Errorf("fakeSignPKG: os.ReadFile: %v", err)) 485 } 486 b, ok := bytes.CutPrefix(b, []byte("I'm a PKG!\n")) 487 if !ok { 488 panic(fmt.Errorf("fakeSignPKG: input doesn't look like a PKG to be signed")) 489 } 490 files, err := tgzToMap(bytes.NewReader(b)) 491 if err != nil { 492 panic(fmt.Errorf("fakeSignPKG: tgzToMap: %v", err)) 493 } 494 for fn, contents := range files { 495 if !strings.Contains(fn, "go/bin") && !strings.Contains(fn, "go/pkg/tool") { 496 continue 497 } 498 files[fn] = contents + msg 499 } 500 b, err = mapToTgz(files) 501 if err != nil { 502 panic(fmt.Errorf("fakeSignPKG: mapToTgz: %v", err)) 503 } 504 b = append([]byte("I'm a PKG! "+msg+"\n"), b...) 505 return s.writeOutput(jobID, path.Base(f), b) 506 } 507 508 func (s *FakeSignService) writeOutput(jobID, base string, contents []byte) string { 509 path := path.Join(s.outputDir, jobID, base) 510 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { 511 panic(fmt.Errorf("fake signing service: os.MkdirAll: %v", err)) 512 } 513 if err := os.WriteFile(path, contents, 0600); err != nil { 514 panic(fmt.Errorf("fake signing service: os.WriteFile: %v", err)) 515 } 516 return "file://" + path 517 } 518 519 func tgzToMap(r io.Reader) (map[string]string, error) { 520 gzr, err := gzip.NewReader(r) 521 if err != nil { 522 return nil, err 523 } 524 defer gzr.Close() 525 526 result := map[string]string{} 527 tr := tar.NewReader(gzr) 528 for { 529 h, err := tr.Next() 530 if err == io.EOF { 531 break 532 } 533 if err != nil { 534 return nil, err 535 } 536 if h.Typeflag != tar.TypeReg { 537 continue 538 } 539 b, err := io.ReadAll(tr) 540 if err != nil { 541 return nil, err 542 } 543 result[h.Name] = string(b) 544 } 545 return result, nil 546 } 547 548 func (s *FakeSignService) fakeSignFile(jobID, f, msg string) string { 549 b, err := os.ReadFile(strings.TrimPrefix(f, "file://")) 550 if err != nil { 551 panic(fmt.Errorf("fakeSignFile: os.ReadFile: %v", err)) 552 } 553 b = append(b, []byte(msg)...) 554 return s.writeOutput(jobID, path.Base(f), b) 555 } 556 557 func (s *FakeSignService) fakeGPGFile(jobID, f string) string { 558 b, err := os.ReadFile(strings.TrimPrefix(f, "file://")) 559 if err != nil { 560 panic(fmt.Errorf("fakeGPGFile: os.ReadFile: %v", err)) 561 } 562 gpg := fmt.Sprintf("I'm a GPG signature for %x!", sha256.Sum256(b)) 563 return s.writeOutput(jobID, path.Base(f)+".asc", []byte(gpg)) 564 } 565 566 var _ CloudBuildClient = (*FakeCloudBuild)(nil) 567 568 const fakeGsutil = ` 569 #!/bin/bash -eux 570 571 case "$1" in 572 "cp") 573 in=$2 574 out=$3 575 if [[ $in == '-' ]]; then 576 in=/dev/stdin 577 fi 578 if [[ $out == '-' ]]; then 579 out=/dev/stdout 580 fi 581 cp "${in#file://}" "${out#file://}" 582 ;; 583 "cat") 584 cat "${2#file://}" 585 ;; 586 *) 587 echo unexpected command $@ >&2 588 exit 1 589 ;; 590 esac 591 ` 592 593 func NewFakeCloudBuild(t *testing.T, gerrit *FakeGerrit, project string, allowedTriggers map[string]map[string]string, fakeGo string) *FakeCloudBuild { 594 toolDir := t.TempDir() 595 if err := os.WriteFile(filepath.Join(toolDir, "go"), []byte(fakeGo), 0777); err != nil { 596 t.Fatal(err) 597 } 598 if err := os.WriteFile(filepath.Join(toolDir, "gsutil"), []byte(fakeGsutil), 0777); err != nil { 599 t.Fatal(err) 600 } 601 return &FakeCloudBuild{ 602 t: t, 603 gerrit: gerrit, 604 project: project, 605 allowedTriggers: allowedTriggers, 606 toolDir: toolDir, 607 results: map[string]error{}, 608 } 609 } 610 611 type FakeCloudBuild struct { 612 t *testing.T 613 gerrit *FakeGerrit 614 project string 615 allowedTriggers map[string]map[string]string 616 toolDir string 617 618 mu sync.Mutex 619 results map[string]error 620 } 621 622 func (cb *FakeCloudBuild) RunBuildTrigger(ctx context.Context, project string, trigger string, substitutions map[string]string) (CloudBuild, error) { 623 if project != cb.project { 624 return CloudBuild{}, fmt.Errorf("unexpected project %v, want %v", project, cb.project) 625 } 626 if allowedSubs, ok := cb.allowedTriggers[trigger]; !ok || !reflect.DeepEqual(allowedSubs, substitutions) { 627 return CloudBuild{}, fmt.Errorf("unexpected trigger %v: got params %#v, want %#v", trigger, substitutions, allowedSubs) 628 } 629 id := fmt.Sprintf("build-%v", rand.Int63()) 630 cb.mu.Lock() 631 cb.results[id] = nil 632 cb.mu.Unlock() 633 return CloudBuild{Project: project, ID: id}, nil 634 } 635 636 func (cb *FakeCloudBuild) Completed(ctx context.Context, build CloudBuild) (string, bool, error) { 637 if build.Project != cb.project { 638 return "", false, fmt.Errorf("unexpected build project: got %q, want %q", build.Project, cb.project) 639 } 640 cb.mu.Lock() 641 result, ok := cb.results[build.ID] 642 cb.mu.Unlock() 643 if !ok { 644 return "", false, fmt.Errorf("unknown build ID %q", build.ID) 645 } 646 return "here's some build detail", true, result 647 } 648 649 func (c *FakeCloudBuild) ResultFS(ctx context.Context, build CloudBuild) (fs.FS, error) { 650 return gcsfs.FromURL(ctx, nil, build.ResultURL) 651 } 652 653 func (cb *FakeCloudBuild) RunScript(ctx context.Context, script string, gerritProject string, outputs []string) (CloudBuild, error) { 654 var wd string 655 if gerritProject != "" { 656 repo, err := cb.gerrit.repo(gerritProject) 657 if err != nil { 658 return CloudBuild{}, err 659 } 660 dir, err := (&Git{}).Clone(ctx, repo.dir.dir) 661 if err != nil { 662 return CloudBuild{}, err 663 } 664 defer dir.Close() 665 wd = dir.dir 666 } else { 667 wd = cb.t.TempDir() 668 } 669 670 tempDir := cb.t.TempDir() 671 cmd := exec.Command("bash", "-eux") 672 cmd.Stdin = strings.NewReader(script) 673 cmd.Dir = wd 674 cmd.Env = os.Environ() 675 cmd.Env = append(cmd.Env, "TEMP="+tempDir, "TMP="+tempDir, "TEMPDIR="+tempDir, "TMPDIR="+tempDir) 676 cmd.Env = append(cmd.Env, "PATH="+cb.toolDir+":/bin:/usr/bin") 677 678 buf := &bytes.Buffer{} 679 cmd.Stdout = buf 680 cmd.Stderr = buf 681 682 runErr := cmd.Run() 683 if runErr != nil { 684 runErr = fmt.Errorf("script failed: %v output:\n%s", runErr, buf.String()) 685 } 686 id := fmt.Sprintf("build-%v", rand.Int63()) 687 resultDir := cb.t.TempDir() 688 if runErr == nil { 689 for _, out := range outputs { 690 target := filepath.Join(resultDir, out) 691 os.MkdirAll(filepath.Dir(target), 0777) 692 if err := os.Rename(filepath.Join(wd, out), target); err != nil { 693 runErr = fmt.Errorf("collecting outputs: %v", err) 694 break 695 } 696 } 697 } 698 cb.mu.Lock() 699 cb.results[id] = runErr 700 cb.mu.Unlock() 701 return CloudBuild{Project: cb.project, ID: id, ResultURL: "file://" + resultDir}, nil 702 } 703 704 type FakeSwarmingClient struct { 705 t *testing.T 706 toolDir string 707 708 mu sync.Mutex 709 results map[string]error 710 } 711 712 func NewFakeSwarmingClient(t *testing.T, fakeGo string) *FakeSwarmingClient { 713 toolDir := t.TempDir() 714 if err := os.WriteFile(filepath.Join(toolDir, "go"), []byte(fakeGo), 0777); err != nil { 715 t.Fatal(err) 716 } 717 if err := os.WriteFile(filepath.Join(toolDir, "gsutil"), []byte(fakeGsutil), 0777); err != nil { 718 t.Fatal(err) 719 } 720 return &FakeSwarmingClient{ 721 t: t, 722 toolDir: toolDir, 723 results: map[string]error{}, 724 } 725 } 726 727 var _ SwarmingClient = (*FakeSwarmingClient)(nil) 728 729 func (c *FakeSwarmingClient) RunTask(ctx context.Context, dims map[string]string, script string, env map[string]string) (string, error) { 730 tempDir := c.t.TempDir() 731 cmd := exec.Command("bash", "-eux") 732 cmd.Stdin = strings.NewReader("set -o pipefail\n" + script) 733 cmd.Dir = c.t.TempDir() 734 cmd.Env = os.Environ() 735 cmd.Env = append(cmd.Env, "TEMP="+tempDir, "TMP="+tempDir, "TEMPDIR="+tempDir, "TMPDIR="+tempDir) 736 cmd.Env = append(cmd.Env, "PATH="+c.toolDir+":/bin:/usr/bin:.") // Note: . is on PATH to help with Windows compatibility 737 for k, v := range env { 738 cmd.Env = append(cmd.Env, k+"="+v) 739 } 740 buf := &bytes.Buffer{} 741 cmd.Stdout = buf 742 cmd.Stderr = buf 743 744 runErr := cmd.Run() 745 if runErr != nil { 746 runErr = fmt.Errorf("script failed: %v output:\n%s", runErr, buf.String()) 747 } 748 id := fmt.Sprintf("build-%v", rand.Int63()) 749 c.mu.Lock() 750 c.results[id] = runErr 751 c.mu.Unlock() 752 return id, nil 753 } 754 755 func (c *FakeSwarmingClient) Completed(ctx context.Context, id string) (string, bool, error) { 756 c.mu.Lock() 757 result, ok := c.results[id] 758 c.mu.Unlock() 759 if !ok { 760 return "", false, fmt.Errorf("unknown task ID %q", id) 761 } 762 return "here's some build detail", true, result 763 } 764 765 func NewFakeBuildBucketClient(major int, url, bucket string, projects []string) *FakeBuildBucketClient { 766 return &FakeBuildBucketClient{ 767 Bucket: bucket, 768 major: major, 769 GerritURL: url, 770 Projects: projects, 771 results: map[int64]error{}, 772 } 773 } 774 775 type FakeBuildBucketClient struct { 776 Bucket string 777 FailBuilds []string 778 MissingBuilds []string 779 major int 780 GerritURL, Branch string 781 Projects []string 782 783 mu sync.Mutex 784 results map[int64]error 785 } 786 787 var _ BuildBucketClient = (*FakeBuildBucketClient)(nil) 788 789 func (c *FakeBuildBucketClient) ListBuilders(ctx context.Context, bucket string) (map[string]*pb.BuilderConfig, error) { 790 if bucket != c.Bucket { 791 return nil, fmt.Errorf("unexpected bucket %q", bucket) 792 } 793 res := map[string]*pb.BuilderConfig{} 794 for _, proj := range c.Projects { 795 prefix := "" 796 if proj != "go" { 797 prefix = "x_" + proj + "-" 798 } 799 for _, v := range []string{"gotip", fmt.Sprintf("go1.%v", c.major)} { 800 for _, b := range []string{"linux-amd64", "linux-amd64-longtest", "darwin-amd64_13"} { 801 parts := strings.FieldsFunc(b, func(r rune) bool { return r == '-' || r == '_' }) 802 res[prefix+v+"-"+b] = &pb.BuilderConfig{ 803 Properties: fmt.Sprintf(`{"project":%q, "is_google":true, "target":{"goos":%q, "goarch":%q}}`, proj, parts[0], parts[1]), 804 } 805 } 806 } 807 } 808 return res, nil 809 } 810 811 func (c *FakeBuildBucketClient) RunBuild(ctx context.Context, bucket string, builder string, commit *pb.GitilesCommit, properties map[string]*structpb.Value) (int64, error) { 812 if bucket != c.Bucket { 813 return 0, fmt.Errorf("unexpected bucket %q", bucket) 814 } 815 match := regexp.MustCompile(`.*://(.+)`).FindStringSubmatch(c.GerritURL) 816 if commit.Host != match[1] || !slices.Contains(c.Projects, commit.Project) { 817 return 0, fmt.Errorf("unexpected host or project: got %q, %q want %q, %q", commit.Host, commit.Project, match[1], c.Projects) 818 } 819 // It would be nice to validate the commit hash and branch, but it's 820 // tricky to get the right value because it depends on the release type. 821 // At least validate the commit is a commit. 822 if len(commit.Id) != 40 { 823 return 0, fmt.Errorf("malformed Git commit hash %q", commit.Id) 824 } 825 var runErr error 826 for _, failBuild := range c.FailBuilds { 827 if strings.Contains(builder, failBuild) { 828 runErr = fmt.Errorf("run of %q is specified to fail", builder) 829 } 830 } 831 832 id := rand.Int63() 833 c.mu.Lock() 834 c.results[id] = runErr 835 c.mu.Unlock() 836 return id, nil 837 } 838 839 func (c *FakeBuildBucketClient) Completed(ctx context.Context, id int64) (string, bool, error) { 840 c.mu.Lock() 841 result, ok := c.results[id] 842 c.mu.Unlock() 843 if !ok { 844 return "", false, fmt.Errorf("unknown task ID %q", id) 845 } 846 return "here's some build detail", true, result 847 } 848 849 func (c *FakeBuildBucketClient) SearchBuilds(ctx context.Context, pred *pb.BuildPredicate) ([]int64, error) { 850 if slices.Contains(c.MissingBuilds, pred.GetBuilder().GetBuilder()) { 851 return nil, nil 852 } 853 return []int64{rand.Int63()}, nil 854 }