blob: c08448373f25b1c0fc12591f7816220d55f5fd0e [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package task
import (
"context"
"flag"
"fmt"
"reflect"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"go.chromium.org/luci/auth"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/grpc/prpc"
"go.chromium.org/luci/hardcoded/chromeinfra"
"golang.org/x/build/gerrit"
"golang.org/x/build/internal/workflow"
wf "golang.org/x/build/internal/workflow"
)
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.")
func TestSelectReposLive(t *testing.T) {
if !*flagRunTagXTest {
t.Skip("Not enabled by flags")
}
tasks := &TagXReposTasks{
Gerrit: &RealGerritClient{
Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth()),
},
}
ctx := &workflow.TaskContext{
Context: context.Background(),
Logger: &testLogger{t, ""},
}
repos, err := tasks.SelectRepos(ctx)
if err != nil {
t.Fatal(err)
}
for _, r := range repos {
t.Logf("%#v", r)
}
}
func TestCycles(t *testing.T) {
deps := func(modPaths ...string) []*TagDep {
var deps = make([]*TagDep, len(modPaths))
for i, p := range modPaths {
deps[i] = &TagDep{p, true}
}
return deps
}
tests := []struct {
repos []TagRepo
want []string
}{
{
repos: []TagRepo{
{Name: "text", Deps: deps("tools")},
{Name: "tools", Deps: deps("text")},
{Name: "sys"},
{Name: "net", Deps: deps("sys")},
},
want: []string{
"tools,text,tools",
"text,tools,text",
},
},
{
repos: []TagRepo{
{Name: "text", Deps: deps("tools")},
{Name: "tools", Deps: deps("fake")},
{Name: "fake", Deps: deps("text")},
},
want: []string{
"tools,fake,text,tools",
"text,tools,fake,text",
"fake,text,tools,fake",
},
},
{
repos: []TagRepo{
{Name: "text", Deps: deps("tools")},
{Name: "tools", Deps: deps("fake", "text")},
{Name: "fake", Deps: deps("tools")},
},
want: []string{
"tools,text,tools",
"text,tools,text",
"tools,fake,tools",
"fake,tools,fake",
},
},
{
repos: []TagRepo{
{Name: "text", Deps: deps("tools")},
{Name: "tools", Deps: deps("fake", "text")},
{Name: "fake1", Deps: deps("fake2")},
{Name: "fake2", Deps: deps("tools")},
},
want: []string{
"tools,text,tools",
"text,tools,text",
},
},
}
for _, tt := range tests {
var repos []TagRepo
for _, r := range tt.repos {
repos = append(repos, TagRepo{
Name: r.Name,
ModPath: r.Name,
Deps: r.Deps,
})
}
cycles := checkCycles(repos)
got := map[string]bool{}
for _, cycle := range cycles {
got[strings.Join(cycle, ",")] = true
}
want := map[string]bool{}
for _, cycle := range tt.want {
want[cycle] = true
}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("%v result unexpected: %v", tt.repos, diff)
}
}
}
var flagRunFindMissingBuildersLiveTest = flag.String("run-find-missing-builders-test", "", "run greenness test for repo@rev")
var flagRunMissingBuilds = flag.Bool("run-missing-builds", false, "run missing builds from missing builders test")
func TestFindMissingBuildersLive(t *testing.T) {
if !testing.Verbose() || flag.Lookup("test.run").Value.String() != "^TestFindMissingBuildersLive$" {
t.Skip("not running a live test requiring manual verification if not explicitly requested with go test -v -run=^TestFindMissingBuildersLive$")
}
repo, commit, ok := strings.Cut(*flagRunFindMissingBuildersLiveTest, "@")
if !ok {
t.Fatalf("-run-find-missing-builders-test flag must be module@rev: %q", *flagRunFindMissingBuildersLiveTest)
}
ctx := &workflow.TaskContext{Context: context.Background(), Logger: &testLogger{t, ""}}
luciHTTPClient, err := auth.NewAuthenticator(ctx, auth.SilentLogin, chromeinfra.DefaultAuthOptions()).Client()
if err != nil {
t.Fatal("auth.NewAuthenticator:", err)
}
buildsClient := buildbucketpb.NewBuildsClient(&prpc.Client{
C: luciHTTPClient,
Host: "cr-buildbucket.appspot.com",
})
buildersClient := buildbucketpb.NewBuildersClient(&prpc.Client{
C: luciHTTPClient,
Host: "cr-buildbucket.appspot.com",
})
tasks := &TagXReposTasks{
Gerrit: &RealGerritClient{
Client: gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth),
Gitiles: "https://go.googlesource.com",
},
BuildBucket: &RealBuildBucketClient{
BuildsClient: buildsClient,
BuildersClient: buildersClient,
},
}
builds, err := tasks.findMissingBuilders(ctx, TagRepo{Name: repo}, commit)
if err != nil {
t.Fatal(err)
}
t.Logf("missing builds for %v at %v: %v", repo, commit, builds)
if !*flagRunMissingBuilds {
return
}
t.Logf("build error (if any): %v", tasks.runMissingBuilders(ctx, TagRepo{Name: repo}, commit, builds))
}
func TestAwaitGreen(t *testing.T) {
tests := []struct {
findBuild, passBuild, pass bool
}{
{findBuild: true, pass: true},
{findBuild: false, passBuild: true, pass: true},
{findBuild: false, passBuild: false, pass: false},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("find_%v_pass_%v", tt.findBuild, tt.passBuild), func(t *testing.T) {
tools := NewFakeRepo(t, "tools")
commit := tools.Commit(map[string]string{
"gopls.go": "I'm gopls!",
})
deps := newTagXTestDeps(t, tools)
if !tt.findBuild {
deps.buildbucket.MissingBuilds = []string{
"x_tools-go1.0-linux-amd64",
}
}
if !tt.passBuild {
deps.buildbucket.FailBuilds = []string{"x_tools-go1.0-linux-amd64"}
}
res, err := deps.tagXTasks.AwaitGreen(deps.ctx, TagRepo{Name: "tools"}, commit)
t.Logf("commit, err = %v, %v", res, err)
if (err == nil) != tt.pass {
t.Fatalf("success = %v (err %v), wanted %v", err == nil, err, tt.pass)
}
if tt.pass && res != commit {
t.Fatalf("green commit = %v, want %v", res, commit)
}
})
}
}
const fakeGo = `#!/bin/bash -exu
case "$1" in
"get")
ls go.mod go.sum >/dev/null
for i in "${@:2}"; do
echo -e "// pretend we've upgraded to $i" >> go.mod
echo "$i h1:asdasd" | tr '@' ' ' >> go.sum
done
;;
"mod")
ls go.mod go.sum >/dev/null
echo "tidied! $*" >> go.mod
;;
*)
echo unexpected command $@
exit 1
;;
esac
`
type tagXTestDeps struct {
ctx *wf.TaskContext
gerrit *FakeGerrit
buildbucket *FakeBuildBucketClient
tagXTasks *TagXReposTasks
}
// mustHaveShell skips if the current environment doesn't support shell
// scripting (/bin/bash).
func mustHaveShell(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("Requires bash shell scripting support.")
}
}
func newTagXTestDeps(t *testing.T, repos ...*FakeRepo) *tagXTestDeps {
mustHaveShell(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
fakeGerrit := NewFakeGerrit(t, repos...)
var projects []string
for _, r := range repos {
projects = append(projects, r.name)
}
fakeBuildBucket := NewFakeBuildBucketClient(0, fakeGerrit.GerritURL(), "ci", projects)
tasks := &TagXReposTasks{
Gerrit: fakeGerrit,
CloudBuild: NewFakeCloudBuild(t, fakeGerrit, "project", nil, fakeGo),
BuildBucket: fakeBuildBucket,
}
return &tagXTestDeps{
ctx: &wf.TaskContext{Context: ctx, Logger: &testLogger{t: t}},
gerrit: fakeGerrit,
buildbucket: fakeBuildBucket,
tagXTasks: tasks,
}
}
func TestTagXRepos(t *testing.T) {
sys := NewFakeRepo(t, "sys")
sys1 := sys.Commit(map[string]string{
"go.mod": "module golang.org/x/sys\n",
"go.sum": "\n",
})
sys.Tag("v0.1.0", sys1)
sys2 := sys.Commit(map[string]string{
"main.go": "package main",
})
mod := NewFakeRepo(t, "mod")
mod1 := mod.Commit(map[string]string{
"go.mod": "module golang.org/x/mod\n",
"go.sum": "\n",
})
mod.Tag("v1.0.0", mod1)
tools := NewFakeRepo(t, "tools")
tools1 := tools.Commit(map[string]string{
"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",
"go.sum": "\n",
"gopls/go.mod": "module golang.org/x/tools/gopls\nrequire golang.org/x/mod v1.0.0\n",
"gopls/go.sum": "\n",
})
tools.Tag("v1.1.5", tools1)
build := NewFakeRepo(t, "build")
build.Commit(map[string]string{
"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",
"go.sum": "\n",
})
deps := newTagXTestDeps(t, sys, mod, tools, build)
wd := deps.tagXTasks.NewDefinition()
w, err := workflow.Start(wd, map[string]interface{}{
reviewersParam.Name: []string(nil),
})
if err != nil {
t.Fatal(err)
}
ctx := deps.ctx
_, err = w.Run(ctx, &verboseListener{t: t})
if err != nil {
t.Fatal(err)
}
tag, err := deps.gerrit.GetTag(ctx, "sys", "v0.2.0")
if err != nil {
t.Fatalf("sys should have been tagged with v0.2.0: %v", err)
}
if tag.Revision != sys2 {
t.Errorf("sys v0.2.0 = %v, want %v", tag.Revision, sys2)
}
tags, err := deps.gerrit.ListTags(ctx, "mod")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tags, []string{"v1.0.0"}) {
t.Errorf("mod has tags %v, wanted only v1.0.0", tags)
}
tag, err = deps.gerrit.GetTag(ctx, "tools", "v1.2.0")
if err != nil {
t.Fatalf("tools should have been tagged with v1.2.0: %v", err)
}
goMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "go.mod")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(goMod), "sys@v0.2.0") || !strings.Contains(string(goMod), "mod@v1.0.0") {
t.Errorf("tools should use sys v0.2.0 and mod v1.0.0. go.mod: %v", string(goMod))
}
if !strings.Contains(string(goMod), "tidied!") {
t.Error("tools go.mod should be tidied")
}
goplsMod, err := deps.gerrit.ReadFile(ctx, "tools", tag.Revision, "gopls/go.mod")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(goplsMod), "tidied!") || !strings.Contains(string(goplsMod), "1.16") || strings.Contains(string(goplsMod), "upgraded") {
t.Error("gopls go.mod should be tidied with -compat 1.16, but not upgraded")
}
tags, err = deps.gerrit.ListTags(ctx, "build")
if err != nil {
t.Fatal(err)
}
if len(tags) != 0 {
t.Errorf("build has tags %q, should not have been tagged", tags)
}
goMod, err = deps.gerrit.ReadFile(ctx, "build", "master", "go.mod")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(goMod), "tools@v1.2.0") || !strings.Contains(string(goMod), "sys@v0.2.0") {
t.Errorf("build should use tools v1.2.0 and sys v0.2.0. go.mod: %v", string(goMod))
}
if !strings.Contains(string(goMod), "tidied!") {
t.Error("build go.mod should be tidied")
}
}
func testTagSingleRepo(t *testing.T, skipPostSubmit bool) {
mod := NewFakeRepo(t, "mod")
mod1 := mod.Commit(map[string]string{
"go.mod": "module golang.org/x/mod\n",
"go.sum": "\n",
})
mod.Tag("v1.1.0", mod1)
foo := NewFakeRepo(t, "foo")
foo1 := foo.Commit(map[string]string{
"go.mod": "module golang.org/x/foo\nrequire golang.org/x/mod v1.0.0\n",
"go.sum": "\n",
})
foo.Tag("v1.1.5", foo1)
foo.Commit(map[string]string{
"main.go": "package main",
})
deps := newTagXTestDeps(t, mod, foo)
deps.buildbucket.MissingBuilds = []string{"x_foo-gotip-linux-amd64"}
args := map[string]interface{}{
"Repository name": "foo",
reviewersParam.Name: []string(nil),
}
if skipPostSubmit {
deps.buildbucket.FailBuilds = []string{"x_foo-gotip-linux-amd64"}
args["Skip post submit result (optional)"] = true
} else {
args["Skip post submit result (optional)"] = false
}
wd := deps.tagXTasks.NewSingleDefinition()
w, err := workflow.Start(wd, args)
if err != nil {
t.Fatal(err)
}
ctx := deps.ctx
_, err = w.Run(ctx, &verboseListener{t: t})
if err != nil {
t.Fatal(err)
}
tag, err := deps.gerrit.GetTag(ctx, "foo", "v1.2.0")
if err != nil {
t.Fatalf("foo should have been tagged with v1.2.0: %v", err)
}
goMod, err := deps.gerrit.ReadFile(ctx, "foo", tag.Revision, "go.mod")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(goMod), "mod@v1.1.0") {
t.Errorf("foo should use mod v1.1.0. go.mod: %v", string(goMod))
}
}
func TestTagSingleRepo(t *testing.T) {
t.Run("with post-submit check", func(t *testing.T) { testTagSingleRepo(t, false) })
// If skipPostSubmit is false, AwaitGreen should sit an spin for a minute before failing
t.Run("without post-submit check", func(t *testing.T) { testTagSingleRepo(t, true) })
}
type verboseListener struct {
t *testing.T
outputListener func(string, interface{})
onStall func()
}
func (l *verboseListener) WorkflowStalled(workflowID uuid.UUID) error {
l.t.Logf("workflow %q: stalled", workflowID.String())
if l.onStall != nil {
l.onStall()
}
return nil
}
func (l *verboseListener) TaskStateChanged(_ uuid.UUID, _ string, st *workflow.TaskState) error {
switch {
case !st.Finished:
l.t.Logf("task %-10v: started", st.Name)
case st.Error != "":
l.t.Logf("task %-10v: error: %v", st.Name, st.Error)
default:
l.t.Logf("task %-10v: done: %v", st.Name, st.Result)
if l.outputListener != nil {
l.outputListener(st.Name, st.Result)
}
}
return nil
}
func (l *verboseListener) Logger(_ uuid.UUID, task string) workflow.Logger {
return &testLogger{t: l.t, task: task}
}
type testLogger struct {
t *testing.T
task string // Optional.
}
func (l *testLogger) Printf(format string, v ...interface{}) {
l.t.Logf("%v\ttask %-10v: LOG: %s", time.Now(), l.task, fmt.Sprintf(format, v...))
}