golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/tagx_test.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 "context" 9 "flag" 10 "fmt" 11 "reflect" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/google/go-cmp/cmp" 18 "github.com/google/uuid" 19 "go.chromium.org/luci/auth" 20 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 21 "go.chromium.org/luci/grpc/prpc" 22 "go.chromium.org/luci/hardcoded/chromeinfra" 23 "golang.org/x/build/gerrit" 24 "golang.org/x/build/internal/workflow" 25 wf "golang.org/x/build/internal/workflow" 26 ) 27 28 var flagRunTagXTest = flag.Bool("run-tagx-test", false, "run tag x/ repo test, which is read-only and safe. Must have a Gerrit cookie in gitcookies.") 29 30 func TestSelectReposLive(t *testing.T) { 31 if !*flagRunTagXTest { 32 t.Skip("Not enabled by flags") 33 } 34 35 tasks := &TagXReposTasks{ 36 Gerrit: &RealGerritClient{ 37 Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth()), 38 }, 39 } 40 ctx := &workflow.TaskContext{ 41 Context: context.Background(), 42 Logger: &testLogger{t, ""}, 43 } 44 repos, err := tasks.SelectRepos(ctx) 45 if err != nil { 46 t.Fatal(err) 47 } 48 for _, r := range repos { 49 t.Logf("%#v", r) 50 } 51 } 52 53 func TestCycles(t *testing.T) { 54 deps := func(modPaths ...string) []*TagDep { 55 var deps = make([]*TagDep, len(modPaths)) 56 for i, p := range modPaths { 57 deps[i] = &TagDep{p, true} 58 } 59 return deps 60 } 61 tests := []struct { 62 repos []TagRepo 63 want []string 64 }{ 65 { 66 repos: []TagRepo{ 67 {Name: "text", Deps: deps("tools")}, 68 {Name: "tools", Deps: deps("text")}, 69 {Name: "sys"}, 70 {Name: "net", Deps: deps("sys")}, 71 }, 72 want: []string{ 73 "tools,text,tools", 74 "text,tools,text", 75 }, 76 }, 77 { 78 repos: []TagRepo{ 79 {Name: "text", Deps: deps("tools")}, 80 {Name: "tools", Deps: deps("fake")}, 81 {Name: "fake", Deps: deps("text")}, 82 }, 83 want: []string{ 84 "tools,fake,text,tools", 85 "text,tools,fake,text", 86 "fake,text,tools,fake", 87 }, 88 }, 89 { 90 repos: []TagRepo{ 91 {Name: "text", Deps: deps("tools")}, 92 {Name: "tools", Deps: deps("fake", "text")}, 93 {Name: "fake", Deps: deps("tools")}, 94 }, 95 want: []string{ 96 "tools,text,tools", 97 "text,tools,text", 98 "tools,fake,tools", 99 "fake,tools,fake", 100 }, 101 }, 102 { 103 repos: []TagRepo{ 104 {Name: "text", Deps: deps("tools")}, 105 {Name: "tools", Deps: deps("fake", "text")}, 106 {Name: "fake1", Deps: deps("fake2")}, 107 {Name: "fake2", Deps: deps("tools")}, 108 }, 109 want: []string{ 110 "tools,text,tools", 111 "text,tools,text", 112 }, 113 }, 114 } 115 116 for _, tt := range tests { 117 var repos []TagRepo 118 for _, r := range tt.repos { 119 repos = append(repos, TagRepo{ 120 Name: r.Name, 121 ModPath: r.Name, 122 Deps: r.Deps, 123 }) 124 } 125 cycles := checkCycles(repos) 126 got := map[string]bool{} 127 for _, cycle := range cycles { 128 got[strings.Join(cycle, ",")] = true 129 } 130 want := map[string]bool{} 131 for _, cycle := range tt.want { 132 want[cycle] = true 133 } 134 135 if diff := cmp.Diff(got, want); diff != "" { 136 t.Errorf("%v result unexpected: %v", tt.repos, diff) 137 } 138 } 139 } 140 141 var flagRunFindMissingBuildersLiveTest = flag.String("run-find-missing-builders-test", "", "run greenness test for repo@rev") 142 var flagRunMissingBuilds = flag.Bool("run-missing-builds", false, "run missing builds from missing builders test") 143 144 func TestFindMissingBuildersLive(t *testing.T) { 145 if !testing.Verbose() || flag.Lookup("test.run").Value.String() != "^TestFindMissingBuildersLive$" { 146 t.Skip("not running a live test requiring manual verification if not explicitly requested with go test -v -run=^TestFindMissingBuildersLive$") 147 } 148 repo, commit, ok := strings.Cut(*flagRunFindMissingBuildersLiveTest, "@") 149 if !ok { 150 t.Fatalf("-run-find-missing-builders-test flag must be module@rev: %q", *flagRunFindMissingBuildersLiveTest) 151 } 152 153 ctx := &workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}} 154 luciHTTPClient, err := auth.NewAuthenticator(ctx, auth.SilentLogin, chromeinfra.DefaultAuthOptions()).Client() 155 if err != nil { 156 t.Fatal("auth.NewAuthenticator:", err) 157 } 158 buildsClient := buildbucketpb.NewBuildsClient(&prpc.Client{ 159 C: luciHTTPClient, 160 Host: "cr-buildbucket.appspot.com", 161 }) 162 buildersClient := buildbucketpb.NewBuildersClient(&prpc.Client{ 163 C: luciHTTPClient, 164 Host: "cr-buildbucket.appspot.com", 165 }) 166 167 tasks := &TagXReposTasks{ 168 Gerrit: &RealGerritClient{ 169 Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth), 170 Gitiles: "https://go.googlesource.com", 171 }, 172 BuildBucket: &RealBuildBucketClient{ 173 BuildsClient: buildsClient, 174 BuildersClient: buildersClient, 175 }, 176 } 177 builds, err := tasks.findMissingBuilders(ctx, TagRepo{Name: repo}, commit) 178 if err != nil { 179 t.Fatal(err) 180 } 181 t.Logf("missing builds for %v at %v: %v", repo, commit, builds) 182 183 if !*flagRunMissingBuilds { 184 return 185 } 186 187 t.Logf("build error (if any): %v", tasks.runMissingBuilders(ctx, TagRepo{Name: repo}, commit, builds)) 188 } 189 190 func TestAwaitGreen(t *testing.T) { 191 tests := []struct { 192 findBuild, passBuild, pass bool 193 }{ 194 {findBuild: true, pass: true}, 195 {findBuild: false, passBuild: true, pass: true}, 196 {findBuild: false, passBuild: false, pass: false}, 197 } 198 199 for _, tt := range tests { 200 t.Run(fmt.Sprintf("find_%v_pass_%v", tt.findBuild, tt.passBuild), func(t *testing.T) { 201 tools := NewFakeRepo(t, "tools") 202 commit := tools.Commit(map[string]string{ 203 "gopls.go": "I'm gopls!", 204 }) 205 deps := newTagXTestDeps(t, tools) 206 if !tt.findBuild { 207 deps.buildbucket.MissingBuilds = []string{ 208 "x_tools-go1.0-linux-amd64", 209 } 210 } 211 if !tt.passBuild { 212 deps.buildbucket.FailBuilds = []string{"x_tools-go1.0-linux-amd64"} 213 } 214 215 res, err := deps.tagXTasks.AwaitGreen(deps.ctx, TagRepo{Name: "tools"}, commit) 216 t.Logf("commit, err = %v, %v", res, err) 217 if (err == nil) != tt.pass { 218 t.Fatalf("success = %v (err %v), wanted %v", err == nil, err, tt.pass) 219 } 220 if tt.pass && res != commit { 221 t.Fatalf("green commit = %v, want %v", res, commit) 222 } 223 }) 224 } 225 } 226 227 const fakeGo = `#!/bin/bash -exu 228 229 case "$1" in 230 "get") 231 ls go.mod go.sum >/dev/null 232 for i in "${@:2}"; do 233 echo -e "// pretend we've upgraded to $i" >> go.mod 234 echo "$i h1:asdasd" | tr '@' ' ' >> go.sum 235 done 236 ;; 237 "mod") 238 ls go.mod go.sum >/dev/null 239 echo "tidied! $*" >> go.mod 240 ;; 241 *) 242 echo unexpected command $@ 243 exit 1 244 ;; 245 esac 246 ` 247 248 type tagXTestDeps struct { 249 ctx *wf.TaskContext 250 gerrit *FakeGerrit 251 buildbucket *FakeBuildBucketClient 252 tagXTasks *TagXReposTasks 253 } 254 255 // mustHaveShell skips if the current environment doesn't support shell 256 // scripting (/bin/bash). 257 func mustHaveShell(t *testing.T) { 258 if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { 259 t.Skip("Requires bash shell scripting support.") 260 } 261 } 262 263 func newTagXTestDeps(t *testing.T, repos ...*FakeRepo) *tagXTestDeps { 264 mustHaveShell(t) 265 266 ctx, cancel := context.WithCancel(context.Background()) 267 t.Cleanup(cancel) 268 269 fakeGerrit := NewFakeGerrit(t, repos...) 270 var projects []string 271 for _, r := range repos { 272 projects = append(projects, r.name) 273 } 274 fakeBuildBucket := NewFakeBuildBucketClient(0, fakeGerrit.GerritURL(), "ci", projects) 275 tasks := &TagXReposTasks{ 276 Gerrit: fakeGerrit, 277 CloudBuild: NewFakeCloudBuild(t, fakeGerrit, "project", nil, fakeGo), 278 BuildBucket: fakeBuildBucket, 279 } 280 return &tagXTestDeps{ 281 ctx: &wf.TaskContext{Context: ctx, Logger: &testLogger{t: t}}, 282 gerrit: fakeGerrit, 283 buildbucket: fakeBuildBucket, 284 tagXTasks: tasks, 285 } 286 } 287 288 func TestTagXRepos(t *testing.T) { 289 sys := NewFakeRepo(t, "sys") 290 sys1 := sys.Commit(map[string]string{ 291 "go.mod": "module golang.org/x/sys\n", 292 "go.sum": "\n", 293 }) 294 sys.Tag("v0.1.0", sys1) 295 sys2 := sys.Commit(map[string]string{ 296 "main.go": "package main", 297 }) 298 mod := NewFakeRepo(t, "mod") 299 mod1 := mod.Commit(map[string]string{ 300 "go.mod": "module golang.org/x/mod\n", 301 "go.sum": "\n", 302 }) 303 mod.Tag("v1.0.0", mod1) 304 tools := NewFakeRepo(t, "tools") 305 tools1 := tools.Commit(map[string]string{ 306 "go.mod": "module golang.org/x/tools\nrequire golang.org/x/mod v1.0.0\ngo 1.18 // tagx:compat 1.16\nrequire golang.org/x/sys v0.1.0\nrequire golang.org/x/build v0.0.0\n", 307 "go.sum": "\n", 308 "gopls/go.mod": "module golang.org/x/tools/gopls\nrequire golang.org/x/mod v1.0.0\n", 309 "gopls/go.sum": "\n", 310 }) 311 tools.Tag("v1.1.5", tools1) 312 build := NewFakeRepo(t, "build") 313 build.Commit(map[string]string{ 314 "go.mod": "module golang.org/x/build\ngo 1.18\nrequire golang.org/x/tools v1.0.0\nrequire golang.org/x/sys v0.1.0\n", 315 "go.sum": "\n", 316 }) 317 318 deps := newTagXTestDeps(t, sys, mod, tools, build) 319 320 wd := deps.tagXTasks.NewDefinition() 321 w, err := workflow.Start(wd, map[string]interface{}{ 322 reviewersParam.Name: []string(nil), 323 }) 324 if err != nil { 325 t.Fatal(err) 326 } 327 ctx := deps.ctx 328 _, err = w.Run(ctx, &verboseListener{t: t}) 329 if err != nil { 330 t.Fatal(err) 331 } 332 333 tag, err := deps.gerrit.GetTag(ctx, "sys", "v0.2.0") 334 if err != nil { 335 t.Fatalf("sys should have been tagged with v0.2.0: %v", err) 336 } 337 if tag.Revision != sys2 { 338 t.Errorf("sys v0.2.0 = %v, want %v", tag.Revision, sys2) 339 } 340 341 tags, err := deps.gerrit.ListTags(ctx, "mod") 342 if err != nil { 343 t.Fatal(err) 344 } 345 if !reflect.DeepEqual(tags, []string{"v1.0.0"}) { 346 t.Errorf("mod has tags %v, wanted only v1.0.0", tags) 347 } 348 349 tag, err = deps.gerrit.GetTag(ctx, "tools", "v1.2.0") 350 if err != nil { 351 t.Fatalf("tools should have been tagged with v1.2.0: %v", err) 352 } 353 goMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "go.mod") 354 if err != nil { 355 t.Fatal(err) 356 } 357 if !strings.Contains(string(goMod), "sys@v0.2.0") || !strings.Contains(string(goMod), "mod@v1.0.0") { 358 t.Errorf("tools should use sys v0.2.0 and mod v1.0.0. go.mod: %v", string(goMod)) 359 } 360 if !strings.Contains(string(goMod), "tidied!") { 361 t.Error("tools go.mod should be tidied") 362 } 363 goplsMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "gopls/go.mod") 364 if err != nil { 365 t.Fatal(err) 366 } 367 if !strings.Contains(string(goplsMod), "tidied!") || !strings.Contains(string(goplsMod), "1.16") || strings.Contains(string(goplsMod), "upgraded") { 368 t.Error("gopls go.mod should be tidied with -compat 1.16, but not upgraded") 369 } 370 371 tags, err = deps.gerrit.ListTags(ctx, "build") 372 if err != nil { 373 t.Fatal(err) 374 } 375 if len(tags) != 0 { 376 t.Errorf("build has tags %q, should not have been tagged", tags) 377 } 378 goMod, err = deps.gerrit.ReadFile(ctx, "build", "master", "go.mod") 379 if err != nil { 380 t.Fatal(err) 381 } 382 if !strings.Contains(string(goMod), "tools@v1.2.0") || !strings.Contains(string(goMod), "sys@v0.2.0") { 383 t.Errorf("build should use tools v1.2.0 and sys v0.2.0. go.mod: %v", string(goMod)) 384 } 385 if !strings.Contains(string(goMod), "tidied!") { 386 t.Error("build go.mod should be tidied") 387 } 388 } 389 390 func testTagSingleRepo(t *testing.T, skipPostSubmit bool) { 391 mod := NewFakeRepo(t, "mod") 392 mod1 := mod.Commit(map[string]string{ 393 "go.mod": "module golang.org/x/mod\n", 394 "go.sum": "\n", 395 }) 396 mod.Tag("v1.1.0", mod1) 397 foo := NewFakeRepo(t, "foo") 398 foo1 := foo.Commit(map[string]string{ 399 "go.mod": "module golang.org/x/foo\nrequire golang.org/x/mod v1.0.0\n", 400 "go.sum": "\n", 401 }) 402 foo.Tag("v1.1.5", foo1) 403 foo.Commit(map[string]string{ 404 "main.go": "package main", 405 }) 406 407 deps := newTagXTestDeps(t, mod, foo) 408 deps.buildbucket.MissingBuilds = []string{"x_foo-gotip-linux-amd64"} 409 410 args := map[string]interface{}{ 411 "Repository name": "foo", 412 reviewersParam.Name: []string(nil), 413 } 414 if skipPostSubmit { 415 deps.buildbucket.FailBuilds = []string{"x_foo-gotip-linux-amd64"} 416 args["Skip post submit result (optional)"] = true 417 } else { 418 args["Skip post submit result (optional)"] = false 419 } 420 421 wd := deps.tagXTasks.NewSingleDefinition() 422 w, err := workflow.Start(wd, args) 423 if err != nil { 424 t.Fatal(err) 425 } 426 ctx := deps.ctx 427 _, err = w.Run(ctx, &verboseListener{t: t}) 428 if err != nil { 429 t.Fatal(err) 430 } 431 432 tag, err := deps.gerrit.GetTag(ctx, "foo", "v1.2.0") 433 if err != nil { 434 t.Fatalf("foo should have been tagged with v1.2.0: %v", err) 435 } 436 goMod, err := deps.gerrit.ReadFile(ctx, "foo", tag.Revision, "go.mod") 437 if err != nil { 438 t.Fatal(err) 439 } 440 if !strings.Contains(string(goMod), "mod@v1.1.0") { 441 t.Errorf("foo should use mod v1.1.0. go.mod: %v", string(goMod)) 442 } 443 } 444 445 func TestTagSingleRepo(t *testing.T) { 446 t.Run("with post-submit check", func(t *testing.T) { testTagSingleRepo(t, false) }) 447 // If skipPostSubmit is false, AwaitGreen should sit an spin for a minute before failing 448 t.Run("without post-submit check", func(t *testing.T) { testTagSingleRepo(t, true) }) 449 } 450 451 type verboseListener struct { 452 t *testing.T 453 outputListener func(string, interface{}) 454 onStall func() 455 } 456 457 func (l *verboseListener) WorkflowStalled(workflowID uuid.UUID) error { 458 l.t.Logf("workflow %q: stalled", workflowID.String()) 459 if l.onStall != nil { 460 l.onStall() 461 } 462 return nil 463 } 464 465 func (l *verboseListener) TaskStateChanged(_ uuid.UUID, _ string, st *workflow.TaskState) error { 466 switch { 467 case !st.Finished: 468 l.t.Logf("task %-10v: started", st.Name) 469 case st.Error != "": 470 l.t.Logf("task %-10v: error: %v", st.Name, st.Error) 471 default: 472 l.t.Logf("task %-10v: done: %v", st.Name, st.Result) 473 if l.outputListener != nil { 474 l.outputListener(st.Name, st.Result) 475 } 476 } 477 return nil 478 } 479 480 func (l *verboseListener) Logger(_ uuid.UUID, task string) workflow.Logger { 481 return &testLogger{t: l.t, task: task} 482 } 483 484 type testLogger struct { 485 t *testing.T 486 task string // Optional. 487 } 488 489 func (l *testLogger) Printf(format string, v ...interface{}) { 490 l.t.Logf("%v\ttask %-10v: LOG: %s", time.Now(), l.task, fmt.Sprintf(format, v...)) 491 }