golang.org/x/build@v0.0.0-20240506185731-218518f32b70/maintner/maintnerd/maintapi/api_test.go (about)

     1  // Copyright 2017 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 maintapi
     6  
     7  import (
     8  	"context"
     9  	"encoding/hex"
    10  	"flag"
    11  	"fmt"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/golang/protobuf/proto"
    19  	"github.com/google/go-cmp/cmp"
    20  	"golang.org/x/build/gerrit"
    21  	"golang.org/x/build/maintner"
    22  	"golang.org/x/build/maintner/godata"
    23  	"golang.org/x/build/maintner/maintnerd/apipb"
    24  	"google.golang.org/grpc"
    25  	"google.golang.org/grpc/codes"
    26  	"google.golang.org/protobuf/testing/protocmp"
    27  )
    28  
    29  func TestGetRef(t *testing.T) {
    30  	c := getGoData(t)
    31  	s := apiService{c: c}
    32  	req := &apipb.GetRefRequest{
    33  		GerritServer:  "go.googlesource.com",
    34  		GerritProject: "go",
    35  		Ref:           "refs/heads/master",
    36  	}
    37  	res, err := s.GetRef(context.Background(), req)
    38  	if err != nil {
    39  		t.Fatal(err)
    40  	}
    41  	if len(res.Value) != 40 {
    42  		t.Errorf("go master ref = %q; want length 40 string", res.Value)
    43  	}
    44  
    45  	// Bogus ref
    46  	req.Ref = "NOT EXIST REF"
    47  	res, err = s.GetRef(context.Background(), req)
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  	if len(res.Value) != 0 {
    52  		t.Errorf("go bogus ref = %q; want empty string", res.Value)
    53  	}
    54  
    55  	// Bogus project
    56  	req.GerritProject = "NOT EXIST PROJ"
    57  	_, err = s.GetRef(context.Background(), req)
    58  	if got, want := fmt.Sprint(err), "unknown gerrit project"; got != want {
    59  		t.Errorf("error for bogus project = %q; want %q", got, want)
    60  	}
    61  }
    62  
    63  var hitGerrit = flag.Bool("hit_gerrit", false, "query production Gerrit in TestFindTryWork")
    64  
    65  func TestFindTryWork(t *testing.T) {
    66  	if !*hitGerrit {
    67  		t.Skip("skipping without flag -hit_gerrit")
    68  	}
    69  	c := getGoData(t)
    70  	s := apiService{c: c}
    71  	req := &apipb.GoFindTryWorkRequest{}
    72  	t0 := time.Now()
    73  	res, err := s.GoFindTryWork(context.Background(), req)
    74  	d0 := time.Since(t0)
    75  	if err != nil {
    76  		t.Fatal(err)
    77  	}
    78  
    79  	// Just for interactive debugging. This is using live data.
    80  	// The stable tests are in TestTryWorkItem and TestTryBotStatus.
    81  	t.Logf("Current:\n%v", proto.MarshalTextString(res))
    82  
    83  	t1 := time.Now()
    84  	res2, err := s.GoFindTryWork(context.Background(), req)
    85  	d1 := time.Since(t1)
    86  	t.Logf("Latency: %v, then %v", d0, d1)
    87  	t.Logf("Cached: equal=%v, err=%v", proto.Equal(res, res2), err)
    88  }
    89  
    90  func TestTryBotStatus(t *testing.T) {
    91  	c := getGoData(t)
    92  	tests := []struct {
    93  		proj      string
    94  		clnum     int32
    95  		msgCutoff int
    96  		wantTry   bool
    97  		wantDone  bool
    98  	}{
    99  		{"go", 51430, 1, true, false},
   100  		{"go", 51430, 2, true, false},
   101  		{"go", 51430, 3, true, true},
   102  
   103  		{"build", 48968, 5, true, false},  // adding trybot (coordinator ignores for "build" repo)
   104  		{"build", 48968, 6, false, false}, // removing it
   105  	}
   106  	for _, tt := range tests {
   107  		cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum)
   108  		if cl == nil {
   109  			t.Errorf("CL %d in %s not found", tt.clnum, tt.proj)
   110  			continue
   111  		}
   112  		old := *cl // save before mutations
   113  		cl.Version = cl.Messages[tt.msgCutoff-1].Version
   114  		cl.Messages = cl.Messages[:tt.msgCutoff]
   115  		gotTry, gotDone := tryBotStatus(cl, false /* not staging */)
   116  		if gotTry != tt.wantTry || gotDone != tt.wantDone {
   117  			t.Errorf("tryBotStatus(%q, %d) after %d messages = try/done %v, %v; want %v, %v",
   118  				tt.proj, tt.clnum, tt.msgCutoff, gotTry, gotDone, tt.wantTry, tt.wantDone)
   119  			for _, msg := range cl.Messages {
   120  				t.Logf("  msg ver=%d, text=%q", msg.Version, msg.Message)
   121  			}
   122  		}
   123  		*cl = old // restore
   124  	}
   125  }
   126  
   127  func TestTryWorkItem(t *testing.T) {
   128  	c := getGoData(t)
   129  	goProj := gerritProject{
   130  		refs: []refHash{
   131  			{"refs/heads/master", gitHash("9995c6b50aa55c1cc1236d1d688929df512dad53")},
   132  			{"refs/heads/release-branch.go1.16", gitHash("e67a58b7cb2b228e04477dfdb1aacd8348e63534")},
   133  			{"refs/heads/release-branch.go1.15", gitHash("72ccabc99449b2cb5bb1438eb90244d55f7b02f5")},
   134  		},
   135  	}
   136  	develVersion := apipb.MajorMinor{
   137  		Major: 1, Minor: 17,
   138  	}
   139  	supportedReleases := []*apipb.GoRelease{
   140  		{
   141  			Major: 1, Minor: 16, Patch: 3,
   142  			TagName:      "go1.16.3",
   143  			TagCommit:    "9baddd3f21230c55f0ad2a10f5f20579dcf0a0bb",
   144  			BranchName:   "release-branch.go1.16",
   145  			BranchCommit: "e67a58b7cb2b228e04477dfdb1aacd8348e63534",
   146  		},
   147  		{
   148  			Major: 1, Minor: 15, Patch: 11,
   149  			TagName:      "go1.15.11",
   150  			TagCommit:    "8c163e85267d146274f68854fe02b4a495586584",
   151  			BranchName:   "release-branch.go1.15",
   152  			BranchCommit: "72ccabc99449b2cb5bb1438eb90244d55f7b02f5",
   153  		},
   154  	}
   155  	tests := []struct {
   156  		proj     string
   157  		clnum    int32
   158  		ci       *gerrit.ChangeInfo
   159  		comments map[string][]gerrit.CommentInfo
   160  		want     *apipb.GerritTryWorkItem
   161  	}{
   162  		// Same Change-Id, different branch:
   163  		{"go", 51430, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   164  			Project:     "go",
   165  			Branch:      "master",
   166  			ChangeId:    "I0bcae339624e7d61037d9ea0885b7bd07491bbb6",
   167  			Commit:      "45a4609c0ae214e448612e0bc0846e2f2682f1b2",
   168  			AuthorEmail: "bradfitz@golang.org",
   169  			GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   170  		}},
   171  		{"go", 51450, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   172  			Project:     "go",
   173  			Branch:      "release-branch.go1.9",
   174  			ChangeId:    "I0bcae339624e7d61037d9ea0885b7bd07491bbb6",
   175  			Commit:      "7320506bc58d3a55eff2c67b2ec65cfa94f7b0a7",
   176  			AuthorEmail: "bradfitz@golang.org",
   177  			GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 9}},
   178  		}},
   179  		// Different project: Tested on tip and two supported releases.
   180  		{"build", 51432, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   181  			Project:     "build",
   182  			Branch:      "master",
   183  			ChangeId:    "I1f71836da7008e58d3e76e2cc3170e96cd57ddf6",
   184  			Commit:      "9251bc9950baff61d95da0761e2e4bfab61ed210",
   185  			AuthorEmail: "bradfitz@golang.org",
   186  			GoCommit: []string{
   187  				"9995c6b50aa55c1cc1236d1d688929df512dad53",
   188  				"e67a58b7cb2b228e04477dfdb1aacd8348e63534",
   189  				"72ccabc99449b2cb5bb1438eb90244d55f7b02f5",
   190  			},
   191  			GoBranch: []string{"master", "release-branch.go1.16", "release-branch.go1.15"},
   192  			GoVersion: []*apipb.MajorMinor{
   193  				{Major: 1, Minor: 17},
   194  				{Major: 1, Minor: 16},
   195  				{Major: 1, Minor: 15},
   196  			},
   197  		}},
   198  
   199  		// Test that a golang.org/x repo TryBot on a branch like
   200  		// "internal-branch.go1.N-suffix" tests with Go 1.N (rather than tip + two supported releases).
   201  		// See issues 28891, 42127, and 36882.
   202  		{"net", 314649, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   203  			Project:     "net",
   204  			Branch:      "internal-branch.go1.16-vendor",
   205  			ChangeId:    "I2c54ce3b2acf1c5efdea66db0595b93a3f5ae5f3",
   206  			Commit:      "3f4a416c7d3b3b41375d159f71ff0a801fc0102b",
   207  			AuthorEmail: "katie@golang.org",
   208  			GoCommit:    []string{"e67a58b7cb2b228e04477dfdb1aacd8348e63534"},
   209  			GoBranch:    []string{"release-branch.go1.16"},
   210  			GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 16}},
   211  		}},
   212  
   213  		// Test that TryBots run on branches of the x/ repositories, other than
   214  		// "master" and "release-branch.go1.N". See issue 37512.
   215  		{"tools", 238259, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   216  			Project:     "tools",
   217  			Branch:      "dev.go2go",
   218  			ChangeId:    "I24950593b517af011a636966cb98b9652d2c4134",
   219  			Commit:      "76e917206452e73dc28cbeb58a15ea8f30487263",
   220  			AuthorEmail: "rstambler@golang.org",
   221  			GoCommit:    []string{"9995c6b50aa55c1cc1236d1d688929df512dad53"},
   222  			GoBranch:    []string{"master"},
   223  			GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   224  		}},
   225  
   226  		// Test that x/tools TryBots on gopls release branches are
   227  		// tested on tip and two supported releases. See issue 46156.
   228  		{"tools", 316773, &gerrit.ChangeInfo{}, nil, &apipb.GerritTryWorkItem{
   229  			Project:     "tools",
   230  			Branch:      "gopls-release-branch.0.6",
   231  			ChangeId:    "I32fd2c0d30854e61109ebd16a05d5099f9074fe5",
   232  			Commit:      "0bb7e5c47b1a31f85d4f173edc878a8e049764a5",
   233  			AuthorEmail: "rstambler@golang.org",
   234  			GoCommit: []string{
   235  				"9995c6b50aa55c1cc1236d1d688929df512dad53",
   236  				"e67a58b7cb2b228e04477dfdb1aacd8348e63534",
   237  				"72ccabc99449b2cb5bb1438eb90244d55f7b02f5",
   238  			},
   239  			GoBranch: []string{"master", "release-branch.go1.16", "release-branch.go1.15"},
   240  			GoVersion: []*apipb.MajorMinor{
   241  				{Major: 1, Minor: 17},
   242  				{Major: 1, Minor: 16},
   243  				{Major: 1, Minor: 15},
   244  			},
   245  		}},
   246  
   247  		// With comments:
   248  		{
   249  			proj:  "go",
   250  			clnum: 201203,
   251  			ci: &gerrit.ChangeInfo{
   252  				CurrentRevision: "f99d33e72efdea68fce39765bc94479b5ebed0a9",
   253  				Revisions: map[string]gerrit.RevisionInfo{
   254  					"f99d33e72efdea68fce39765bc94479b5ebed0a9": {PatchSetNumber: 88},
   255  				},
   256  				Messages: []gerrit.ChangeMessageInfo{
   257  					{
   258  						Author:         &gerrit.AccountInfo{NumericID: 1234},
   259  						Message:        "Patch Set 1: Run-TryBot+1\n\n(1 comment)",
   260  						Time:           gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 27, 23, 0, time.UTC)),
   261  						RevisionNumber: 1,
   262  					},
   263  					{
   264  						Author:         &gerrit.AccountInfo{NumericID: 5678},
   265  						Message:        "Patch Set 2: Foo-2 Run-TryBot+1\n\n(1 comment)",
   266  						Time:           gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 28, 47, 0, time.UTC)),
   267  						RevisionNumber: 2,
   268  					},
   269  				},
   270  			},
   271  			comments: map[string][]gerrit.CommentInfo{
   272  				"/PATCHSET_LEVEL": {
   273  					{
   274  						PatchSet: 1,
   275  						Message:  "TRY=foo",
   276  						Updated:  gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 27, 23, 0, time.UTC)),
   277  						Author:   &gerrit.AccountInfo{NumericID: 1234},
   278  					},
   279  					{
   280  						PatchSet: 2,
   281  						Message:  "A preceding sentence.\nTRY=bar, baz\nA following sentence.",
   282  						Updated:  gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 28, 47, 0, time.UTC)),
   283  						Author:   &gerrit.AccountInfo{NumericID: 5678},
   284  					},
   285  				},
   286  			},
   287  			want: &apipb.GerritTryWorkItem{
   288  				Project:     "go",
   289  				Branch:      "master",
   290  				ChangeId:    "I358eb7b11768df8c80fb7e805abd4cd01d52bb9b",
   291  				Commit:      "f99d33e72efdea68fce39765bc94479b5ebed0a9",
   292  				AuthorEmail: "bradfitz@golang.org",
   293  				Version:     88,
   294  				GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   295  				TryMessage: []*apipb.TryVoteMessage{
   296  					{Message: "foo", AuthorId: 1234, Version: 1},
   297  					{Message: "bar, baz", AuthorId: 5678, Version: 2},
   298  				},
   299  			},
   300  		},
   301  
   302  		// Test that followup TRY= requests on the same patch set are included. See issue 42084.
   303  		{
   304  			proj:  "go",
   305  			clnum: 324763,
   306  			ci: &gerrit.ChangeInfo{
   307  				CurrentRevision: "dd38fd80c3667f891dbe06bd1d8ed153c2e208da",
   308  				Revisions: map[string]gerrit.RevisionInfo{
   309  					"dd38fd80c3667f891dbe06bd1d8ed153c2e208da": {PatchSetNumber: 1},
   310  				},
   311  				Messages: []gerrit.ChangeMessageInfo{
   312  					{
   313  						Author:         &gerrit.AccountInfo{NumericID: 1234},
   314  						Message:        "Patch Set 1: Run-TryBot+1 Trust+1\n\n(1 comment)",
   315  						Time:           gerrit.TimeStamp(time.Date(2021, 6, 3, 18, 58, 0, 0, time.UTC)),
   316  						RevisionNumber: 1,
   317  					},
   318  					{
   319  						Author:         &gerrit.AccountInfo{NumericID: 1234},
   320  						Message:        "Patch Set 1: Run-TryBot+1\n\n(1 comment)",
   321  						Time:           gerrit.TimeStamp(time.Date(2021, 6, 3, 19, 16, 26, 0, time.UTC)),
   322  						RevisionNumber: 1,
   323  					},
   324  				},
   325  			},
   326  			comments: map[string][]gerrit.CommentInfo{
   327  				"/PATCHSET_LEVEL": {
   328  					{
   329  						PatchSet: 1,
   330  						Message:  "TRY=windows-arm64,windows-amd64",
   331  						Updated:  gerrit.TimeStamp(time.Date(2021, 6, 3, 18, 58, 0, 0, time.UTC)),
   332  						Author:   &gerrit.AccountInfo{NumericID: 1234},
   333  					},
   334  					{
   335  						PatchSet: 1,
   336  						Message:  "TRY=windows-arm64-10",
   337  						Updated:  gerrit.TimeStamp(time.Date(2021, 6, 3, 19, 16, 26, 0, time.UTC)),
   338  						Author:   &gerrit.AccountInfo{NumericID: 1234},
   339  					},
   340  				},
   341  			},
   342  			want: &apipb.GerritTryWorkItem{
   343  				Project:     "go",
   344  				Branch:      "master",
   345  				ChangeId:    "I023d5208374f867552ba68b45011f7990159868f",
   346  				Commit:      "dd38fd80c3667f891dbe06bd1d8ed153c2e208da",
   347  				AuthorEmail: "thanm@google.com",
   348  				Version:     1,
   349  				GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   350  				TryMessage: []*apipb.TryVoteMessage{
   351  					{Message: "windows-arm64,windows-amd64", AuthorId: 1234, Version: 1},
   352  					{Message: "windows-arm64-10", AuthorId: 1234, Version: 1},
   353  				},
   354  			},
   355  		},
   356  
   357  		// Test that TRY= request messages with an older patchset-level comment are included.
   358  		// See https://go-review.googlesource.com/c/go/+/493535/comments/c72580be_773332cb where
   359  		// a Run-TryBot+1 request is posted on PS 2 with a patchset-level comment left on PS 1.
   360  		{
   361  			proj:  "go",
   362  			clnum: 493535,
   363  			ci: &gerrit.ChangeInfo{
   364  				CurrentRevision: "f8aa751e53d7019eb1114da68754c77cc0830163",
   365  				Revisions: map[string]gerrit.RevisionInfo{
   366  					"a2afb09fc37fcff8ff43d895def78274d6ec4d74": {PatchSetNumber: 1},
   367  					"f8aa751e53d7019eb1114da68754c77cc0830163": {PatchSetNumber: 2},
   368  				},
   369  				Messages: []gerrit.ChangeMessageInfo{
   370  					// A message posted a minute after PS 2 was uploaded.
   371  					{
   372  						Author:         &gerrit.AccountInfo{NumericID: 1234},
   373  						Message:        "Patch Set 2: Code-Review+2 Run-TryBot+1\n\n(1 comment)",
   374  						Time:           gerrit.TimeStamp(time.Date(2023, 5, 8, 16, 14, 3, 0, time.UTC)),
   375  						RevisionNumber: 2,
   376  					},
   377  				},
   378  			},
   379  			comments: map[string][]gerrit.CommentInfo{
   380  				"/PATCHSET_LEVEL": {
   381  					// Its patchset-level comment is associated with PS 1.
   382  					{
   383  						PatchSet: 1,
   384  						Message:  "TRY\u003dplan9\n\nThanks!",
   385  						Updated:  gerrit.TimeStamp(time.Date(2023, 5, 8, 16, 14, 3, 0, time.UTC)),
   386  						Author:   &gerrit.AccountInfo{NumericID: 1234},
   387  					},
   388  				},
   389  			},
   390  			want: &apipb.GerritTryWorkItem{
   391  				Project:     "go",
   392  				Branch:      "master",
   393  				ChangeId:    "Ia30f51307cc6d07a7e3ada6bf9d60bf9951982ff",
   394  				Commit:      "f8aa751e53d7019eb1114da68754c77cc0830163",
   395  				AuthorEmail: "millerresearch@gmail.com",
   396  				Version:     2,
   397  				GoVersion:   []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   398  				TryMessage: []*apipb.TryVoteMessage{
   399  					{Message: "plan9", AuthorId: 1234, Version: 2},
   400  				},
   401  			},
   402  		},
   403  	}
   404  	for _, tt := range tests {
   405  		t.Run(strconv.Itoa(int(tt.clnum)), func(t *testing.T) {
   406  			cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum)
   407  			if cl == nil {
   408  				t.Fatalf("CL %d in %s not found", tt.clnum, tt.proj)
   409  			}
   410  			work, err := tryWorkItem(cl, tt.ci, tt.comments, goProj, develVersion, supportedReleases)
   411  			if err != nil {
   412  				t.Fatalf("tryWorkItem(%q, %v, ...): err=%v", tt.proj, tt.clnum, err)
   413  			}
   414  			if len(work.GoVersion) == 0 {
   415  				t.Errorf("tryWorkItem(%q, %v, ...): len(GoVersion) is zero, want at least one", tt.proj, tt.clnum)
   416  			}
   417  			if work.Project != "go" && (len(work.GoCommit) == 0 || len(work.GoBranch) == 0) {
   418  				t.Errorf("tryWorkItem(%q, %v, ...): GoCommit/GoBranch slice is empty for x/ repo, want both non-empty", tt.proj, tt.clnum)
   419  			}
   420  			if len(work.GoBranch) != len(work.GoCommit) {
   421  				t.Errorf("tryWorkItem(%q, %v, ...): bad correlation between GoBranch and GoCommit slices", tt.proj, tt.clnum)
   422  			}
   423  			if ok := len(work.GoVersion) == len(work.GoCommit) || (len(work.GoVersion) == 1 && len(work.GoCommit) == 0); !ok {
   424  				t.Errorf("tryWorkItem(%q, %v, ...): bad correlation between GoVersion and GoCommit slices", tt.proj, tt.clnum)
   425  			}
   426  			if diff := cmp.Diff(tt.want, work, protocmp.Transform()); diff != "" {
   427  				t.Errorf("tryWorkItem(%q, %v, ...) mismatch (-want +got):\n%s", tt.proj, tt.clnum, diff)
   428  			}
   429  		})
   430  	}
   431  }
   432  
   433  func TestParseInternalBranchVersion(t *testing.T) {
   434  	tests := []struct {
   435  		name    string
   436  		wantMaj int32
   437  		wantMin int32
   438  		wantOK  bool
   439  	}{
   440  		{"internal-branch.go1.16-vendor", 1, 16, true},
   441  		{"internal-branch.go1.16-", 0, 0, false}, // Empty suffix is rejected.
   442  		{"internal-branch.go1.16", 0, 0, false},  // No suffix is rejected.
   443  		{"not-internal-branch", 0, 0, false},
   444  		{"internal-branch.go1.16.2", 0, 0, false},
   445  		{"internal-branch.go42-suffix", 42, 0, true}, // Be ready in case Go 42 is released after 7.5 million years.
   446  	}
   447  	for _, tt := range tests {
   448  		t.Run(tt.name, func(t *testing.T) {
   449  			maj, min, ok := parseInternalBranchVersion(tt.name)
   450  			if ok != tt.wantOK || maj != tt.wantMaj || min != tt.wantMin {
   451  				t.Errorf("parseInternalBranchVersion(%q) = Go %v.%v ok=%v; want Go %v.%v ok=%v", tt.name,
   452  					maj, min, ok, tt.wantMaj, tt.wantMin, tt.wantOK)
   453  			}
   454  		})
   455  	}
   456  }
   457  
   458  var (
   459  	corpusMu    sync.Mutex
   460  	corpusCache *maintner.Corpus
   461  )
   462  
   463  func getGoData(tb testing.TB) *maintner.Corpus {
   464  	if testing.Short() {
   465  		tb.Skip("skipping test requiring large download in short mode")
   466  	}
   467  	corpusMu.Lock()
   468  	defer corpusMu.Unlock()
   469  	if corpusCache != nil {
   470  		return corpusCache
   471  	}
   472  	var err error
   473  	corpusCache, err = godata.Get(context.Background())
   474  	if err != nil {
   475  		tb.Fatalf("getting corpus: %v", err)
   476  	}
   477  	return corpusCache
   478  }
   479  
   480  func TestSupportedGoReleases(t *testing.T) {
   481  	tests := []struct {
   482  		goProj nonChangeRefLister
   483  		want   []*apipb.GoRelease
   484  	}{
   485  		// A sample of real data from maintner.
   486  		{
   487  			goProj: gerritProject{
   488  				refs: []refHash{
   489  					{"HEAD", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")},
   490  					{"refs/heads/dev.boringcrypto", gitHash("13bf5b80e8d8841a2a3c9b0d5dec65a0c8636253")},
   491  					{"refs/heads/dev.boringcrypto.go1.10", gitHash("2e2a04a605b6c3fc6e733810bdcd0200d8ed25a8")},
   492  					{"refs/heads/dev.boringcrypto.go1.11", gitHash("685dc1638240af70c86a146b0ddb86d51d64f269")},
   493  					{"refs/heads/dev.typealias", gitHash("8a5ef1501dee0715093e87cdc1c9b6becb81c882")},
   494  					{"refs/heads/master", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")},
   495  					{"refs/heads/release-branch.go1", gitHash("08b97d4061dd75ceec1d44e4335183cd791c9306")},
   496  					{"refs/heads/release-branch.go1.1", gitHash("1d6d8fca241bb611af51e265c1b5a2e9ae904702")},
   497  					{"refs/heads/release-branch.go1.10", gitHash("e97b7d68f107ff60152f5bd5701e0286f221ee93")},
   498  					{"refs/heads/release-branch.go1.11", gitHash("97781d2ed116d2cd9cb870d0b84fc0ec598c9abc")},
   499  					{"refs/heads/release-branch.go1.10-security", gitHash("25ca8f49c3fc4a68daff7a23ab613e3453be5cda")},
   500  					{"refs/heads/release-branch.go1.11-security", gitHash("90c896448691b5edb0ab11110f37234f63cd28ed")},
   501  					{"refs/heads/release-branch.go1.2", gitHash("43d00b0942c1c6f43993ac71e1eea48e62e22b8d")},
   502  					{"refs/heads/release-branch.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")},
   503  					{"refs/heads/release-branch.r60", gitHash("394b383a1ee0ac3fec5e453a7dbe590d3ce6d6b0")},
   504  					{"refs/notes/review", gitHash("c46ab9dacb2ac618d86f1c1f719bc2de46010e86")},
   505  					{"refs/tags/1.10beta1.mailed", gitHash("2df74db61620771e4f878c9e1db7aeecc00808ba")},
   506  					{"refs/tags/andybons/blog.mailed", gitHash("707a89416af909a3af6c26df93995bc17bf9ce81")},
   507  					{"refs/tags/go1", gitHash("6174b5e21e73714c63061e66efdbe180e1c5491d")},
   508  					{"refs/tags/go1.0.1", gitHash("2fffba7fe19690e038314d17a117d6b87979c89f")},
   509  					{"refs/tags/go1.0.2", gitHash("cb6c6570b73a1c4d19cad94570ed277f7dae55ac")},
   510  					{"refs/tags/go1.0.3", gitHash("30be9b4313622c2077539e68826194cb1028c691")},
   511  					{"refs/tags/go1.1", gitHash("205f850ceacfc39d1e9d76a9569416284594ce8c")},
   512  					{"refs/tags/go1.10", gitHash("bf86aec25972f3a100c3aa58a6abcbcc35bdea49")},
   513  					{"refs/tags/go1.10.1", gitHash("ac7c0ee26dda18076d5f6c151d8f920b43340ae3")},
   514  					{"refs/tags/go1.10.2", gitHash("71bdbf431b79dff61944f22c25c7e085ccfc25d5")},
   515  					{"refs/tags/go1.10.3", gitHash("fe8a0d12b14108cbe2408b417afcaab722b0727c")},
   516  					{"refs/tags/go1.10.4", gitHash("2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78")},
   517  					{"refs/tags/go1.10beta1", gitHash("9ce6b5c2ed5d3d5251b9a6a0c548d5fb2c8567e8")},
   518  					{"refs/tags/go1.10beta2", gitHash("594668a5a96267a46282ce3007a584ec07adf705")},
   519  					{"refs/tags/go1.10rc1", gitHash("5348aed83e39bd1d450d92d7f627e994c2db6ebf")},
   520  					{"refs/tags/go1.10rc2", gitHash("20e228f2fdb44350c858de941dff4aea9f3127b8")},
   521  					{"refs/tags/go1.11", gitHash("41e62b8c49d21659b48a95216e3062032285250f")},
   522  					{"refs/tags/go1.11.1", gitHash("26957168c4c0cdcc7ca4f0b19d0eb19474d224ac")},
   523  					{"refs/tags/go1.11beta1", gitHash("a12c1f26e4cc602dae62ec065a237172a5b8f926")},
   524  					{"refs/tags/go1.11beta2", gitHash("c814ac44c0571f844718f07aa52afa47e37fb1ed")},
   525  					{"refs/tags/go1.11beta3", gitHash("1b870077c896379c066b41657d3c9062097a6943")},
   526  					{"refs/tags/go1.11rc1", gitHash("807e7f2420c683384dc9c6db498808ba1b7aab17")},
   527  					{"refs/tags/go1.11rc2", gitHash("02c0c32960f65d0b9c66ec840c612f5f9623dc51")},
   528  					{"refs/tags/go1.9.7", gitHash("7df09b4a03f9e53334672674ba7983d5e7128646")},
   529  					{"refs/tags/go1.9beta1", gitHash("952ecbe0a27aadd184ca3e2c342beb464d6b1653")},
   530  					{"refs/tags/go1.9beta2", gitHash("eab99a8d548f8ba864647ab171a44f0a5376a6b3")},
   531  					{"refs/tags/go1.9rc1", gitHash("65c6c88a9442b91d8b2fd0230337b1fda4bb6cdf")},
   532  					{"refs/tags/go1.9rc2", gitHash("048c9cfaacb6fe7ac342b0acd8ca8322b6c49508")},
   533  					{"refs/tags/release.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")},
   534  					{"refs/tags/release.r60", gitHash("5464bfebe723752dfc09a6dd6b361b8e79db5995")},
   535  					{"refs/tags/release.r60.1", gitHash("4af7136fcf874e212d66c72178a68db969918b25")},
   536  					{"refs/tags/weekly", gitHash("3895b5051df256b442d0b0af50debfffd8d75164")},
   537  					{"refs/tags/weekly.2009-11-10", gitHash("78c47c36b2984058c1bec0bd72e0b127b24fcd44")},
   538  					{"refs/tags/weekly.2009-11-10.1", gitHash("c57054f7b49539ca4ed6533267c1c20c39aaaaa5")},
   539  				},
   540  			},
   541  			want: []*apipb.GoRelease{
   542  				{
   543  					Major: 1, Minor: 11, Patch: 1,
   544  					TagName:      "go1.11.1",
   545  					TagCommit:    "26957168c4c0cdcc7ca4f0b19d0eb19474d224ac",
   546  					BranchName:   "release-branch.go1.11",
   547  					BranchCommit: "97781d2ed116d2cd9cb870d0b84fc0ec598c9abc",
   548  				},
   549  				{
   550  					Major: 1, Minor: 10, Patch: 4,
   551  					TagName:      "go1.10.4",
   552  					TagCommit:    "2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78",
   553  					BranchName:   "release-branch.go1.10",
   554  					BranchCommit: "e97b7d68f107ff60152f5bd5701e0286f221ee93",
   555  				},
   556  			},
   557  		},
   558  
   559  		// Detect and handle a new major version.
   560  		{
   561  			goProj: gerritProject{
   562  				refs: []refHash{
   563  					{"refs/tags/go1.5", gitHash("9b82ca331d1fa30e3428e7914ba780ae7f75a702")},
   564  					{"refs/tags/go1.42.1", gitHash("23982c09ae5ac811d1dd0099e1626596ade61000")},
   565  					{"refs/tags/go1", gitHash("5c503fde0aa534d3259533802052f936c95fa782")},
   566  					{"refs/tags/go2", gitHash("43126518de2eb0dadc0917a593f08637318986bf")},
   567  					{"refs/tags/go1.11.111", gitHash("c59f000d9bb66592ff84a942014afd1a7be4c953")}, // The onesiest release ever!
   568  					{"refs/heads/release-branch.go1", gitHash("b0f2d801c19fc8798ecf67e50364a44dba606fcd")},
   569  					{"refs/heads/release-branch.go1.5", gitHash("a6ae58c93408bcc17758d397eed0ace973de8481")},
   570  					{"refs/heads/release-branch.go1.11", gitHash("f4f148ef7962271ff8ffcebf13400ded535e9957")},
   571  					{"refs/heads/release-branch.go1.42", gitHash("362986e7a4b5edc911ed55324c37106c40abe3fb")},
   572  					{"refs/heads/release-branch.go2", gitHash("cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59")},
   573  					{"refs/heads/release-branch.go1.2", gitHash("6523e1eb33ef792df04e08462ed332b95311261e")},
   574  
   575  					// It doesn't count as a release if there's no corresponding release-branch.go1.43 release branch.
   576  					{"refs/tags/go1.43", gitHash("3aa7f7065ecf717b1dd6512bb7a9f40625fc8cb5")},
   577  				},
   578  			},
   579  			want: []*apipb.GoRelease{
   580  				{
   581  					Major: 2, Minor: 0, Patch: 0,
   582  					TagName:      "go2",
   583  					TagCommit:    "43126518de2eb0dadc0917a593f08637318986bf",
   584  					BranchName:   "release-branch.go2",
   585  					BranchCommit: "cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59",
   586  				},
   587  				{
   588  					Major: 1, Minor: 42, Patch: 1,
   589  					TagName:      "go1.42.1",
   590  					TagCommit:    "23982c09ae5ac811d1dd0099e1626596ade61000",
   591  					BranchName:   "release-branch.go1.42",
   592  					BranchCommit: "362986e7a4b5edc911ed55324c37106c40abe3fb",
   593  				},
   594  			},
   595  		},
   596  	}
   597  	for i, tt := range tests {
   598  		got, err := supportedGoReleases(tt.goProj)
   599  		if err != nil {
   600  			t.Fatalf("%d: supportedGoReleases: %v", i, err)
   601  		}
   602  		if diff := cmp.Diff(got, tt.want, protocmp.Transform()); diff != "" {
   603  			t.Errorf("%d: supportedGoReleases: (-got +want)\n%s", i, diff)
   604  		}
   605  	}
   606  }
   607  
   608  func TestGetDashboard(t *testing.T) {
   609  	c := getGoData(t)
   610  	s := apiService{c: c}
   611  
   612  	type check func(t *testing.T, res *apipb.DashboardResponse, resErr error)
   613  	var noError check = func(t *testing.T, res *apipb.DashboardResponse, resErr error) {
   614  		t.Helper()
   615  		if resErr != nil {
   616  			t.Fatalf("GetDashboard: %v", resErr)
   617  		}
   618  	}
   619  	var commitsTruncated check = func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   620  		t.Helper()
   621  		if !res.CommitsTruncated {
   622  			t.Errorf("CommitsTruncated = false; want true")
   623  		}
   624  		if len(res.Commits) == 0 {
   625  			t.Errorf("no commits; expected some commits when expecting CommitsTruncated")
   626  		}
   627  
   628  	}
   629  	hasBranch := func(branch string) check {
   630  		return func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   631  			ok := false
   632  			for _, b := range res.Branches {
   633  				if b == branch {
   634  					ok = true
   635  					break
   636  				}
   637  			}
   638  			if !ok {
   639  				t.Errorf("didn't find expected branch %q; got branches: %q", branch, res.Branches)
   640  			}
   641  		}
   642  	}
   643  	hasRepoHead := func(proj string) check {
   644  		return func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   645  			ok := false
   646  			var got []string
   647  			for _, rh := range res.RepoHeads {
   648  				if rh.GerritProject == proj {
   649  					ok = true
   650  				}
   651  				got = append(got, rh.GerritProject)
   652  			}
   653  			if !ok {
   654  				t.Errorf("didn't find expected repo head %q; got: %q", proj, got)
   655  			}
   656  		}
   657  	}
   658  	var hasThreeReleases check = func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   659  		t.Helper()
   660  		var got []string
   661  		var gotMaster int
   662  		var gotReleaseBranch int
   663  		var uniq = map[string]bool{}
   664  		for _, r := range res.Releases {
   665  			got = append(got, r.BranchName)
   666  			uniq[r.BranchName] = true
   667  			if r.BranchName == "master" {
   668  				gotMaster++
   669  			}
   670  			if strings.HasPrefix(r.BranchName, "release-branch.go") {
   671  				gotReleaseBranch++
   672  			}
   673  		}
   674  		if len(uniq) != 3 {
   675  			t.Errorf("expected 3 Go releases, got: %q", got)
   676  		}
   677  		if gotMaster != 1 {
   678  			t.Errorf("expected 1 Go release to be master, got: %q", got)
   679  		}
   680  		if gotReleaseBranch != 2 {
   681  			t.Errorf("expected 2 Go releases to be release branches, got: %q", got)
   682  		}
   683  	}
   684  	wantRPCError := func(code codes.Code) check {
   685  		return func(t *testing.T, _ *apipb.DashboardResponse, err error) {
   686  			if grpc.Code(err) != code {
   687  				t.Errorf("expected RPC code %v; got %v (err %v)", code, grpc.Code(err), err)
   688  			}
   689  		}
   690  	}
   691  	basicChecks := []check{
   692  		noError,
   693  		commitsTruncated,
   694  		hasBranch("master"),
   695  		hasBranch("release-branch.go1.4"),
   696  		hasBranch("release-branch.go1.13"),
   697  		hasRepoHead("net"),
   698  		hasRepoHead("sys"),
   699  		hasThreeReleases,
   700  	}
   701  
   702  	tests := []struct {
   703  		name   string
   704  		req    *apipb.DashboardRequest
   705  		checks []check
   706  	}{
   707  		// Verify that the default view (with no options) works.
   708  		{
   709  			name:   "zero_value",
   710  			req:    &apipb.DashboardRequest{},
   711  			checks: basicChecks,
   712  		},
   713  		// Or with explicit values:
   714  		{
   715  			name: "zero_value_effectively",
   716  			req: &apipb.DashboardRequest{
   717  				Repo:   "go",
   718  				Branch: "master",
   719  			},
   720  			checks: basicChecks,
   721  		},
   722  		// Max commits:
   723  		{
   724  			name: "max_commits",
   725  			req:  &apipb.DashboardRequest{MaxCommits: 1},
   726  			checks: []check{
   727  				noError,
   728  				commitsTruncated,
   729  				func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   730  					if got, want := len(res.Commits), 1; got != want {
   731  						t.Errorf("got %v commits; want %v", got, want)
   732  					}
   733  				},
   734  			},
   735  		},
   736  		// Verify that branch=mixed doesn't return an error at least.
   737  		{
   738  			name: "mixed",
   739  			req:  &apipb.DashboardRequest{Branch: "mixed"},
   740  			checks: []check{
   741  				noError,
   742  				commitsTruncated,
   743  				hasRepoHead("sys"),
   744  				hasThreeReleases,
   745  			},
   746  		},
   747  		// Verify non-Go repos:
   748  		{
   749  			name: "non_go_repo",
   750  			req:  &apipb.DashboardRequest{Repo: "golang.org/x/net"},
   751  			checks: []check{
   752  				noError,
   753  				commitsTruncated,
   754  				func(t *testing.T, res *apipb.DashboardResponse, _ error) {
   755  					for _, c := range res.Commits {
   756  						if c.GoCommitAtTime == "" {
   757  							t.Errorf("response contains commit without GoCommitAtTime")
   758  						}
   759  						if c.GoCommitLatest == "" {
   760  							t.Errorf("response contains commit without GoCommitLatest")
   761  						}
   762  						if t.Failed() {
   763  							return
   764  						}
   765  					}
   766  				},
   767  			},
   768  		},
   769  
   770  		// Validate rejection of bad requests:
   771  		{
   772  			name:   "bad-repo",
   773  			req:    &apipb.DashboardRequest{Repo: "NOT_EXIST"},
   774  			checks: []check{wantRPCError(codes.NotFound)},
   775  		},
   776  		{
   777  			name:   "bad-branch",
   778  			req:    &apipb.DashboardRequest{Branch: "NOT_EXIST"},
   779  			checks: []check{wantRPCError(codes.NotFound)},
   780  		},
   781  		{
   782  			name:   "mixed-with-pagination",
   783  			req:    &apipb.DashboardRequest{Branch: "mixed", Page: 5},
   784  			checks: []check{wantRPCError(codes.InvalidArgument)},
   785  		},
   786  		{
   787  			name:   "negative-page",
   788  			req:    &apipb.DashboardRequest{Page: -1},
   789  			checks: []check{wantRPCError(codes.InvalidArgument)},
   790  		},
   791  		{
   792  			name:   "too-big-page",
   793  			req:    &apipb.DashboardRequest{Page: 1e6},
   794  			checks: []check{wantRPCError(codes.InvalidArgument)},
   795  		},
   796  	}
   797  
   798  	for _, tt := range tests {
   799  		t.Run(tt.name, func(t *testing.T) {
   800  			res, err := s.GetDashboard(context.Background(), tt.req)
   801  			for _, c := range tt.checks {
   802  				c(t, res, err)
   803  			}
   804  		})
   805  	}
   806  }
   807  
   808  type gerritProject struct {
   809  	refs []refHash
   810  }
   811  
   812  func (gp gerritProject) Ref(ref string) maintner.GitHash {
   813  	for _, r := range gp.refs {
   814  		if r.Ref == ref {
   815  			return r.Hash
   816  		}
   817  	}
   818  	return ""
   819  }
   820  
   821  func (gp gerritProject) ForeachNonChangeRef(fn func(ref string, hash maintner.GitHash) error) error {
   822  	for _, r := range gp.refs {
   823  		err := fn(r.Ref, r.Hash)
   824  		if err != nil {
   825  			return err
   826  		}
   827  	}
   828  	return nil
   829  }
   830  
   831  type refHash struct {
   832  	Ref  string
   833  	Hash maintner.GitHash
   834  }
   835  
   836  func gitHash(hexa string) maintner.GitHash {
   837  	if len(hexa) != 40 {
   838  		panic(fmt.Errorf("bogus git hash %q", hexa))
   839  	}
   840  	binary, err := hex.DecodeString(hexa)
   841  	if err != nil {
   842  		panic(fmt.Errorf("bogus git hash %q: %v", hexa, err))
   843  	}
   844  	return maintner.GitHash(binary)
   845  }