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  }