blob: 2c7b429601d0e5c63c8e54208327bff7b4fdd89d [file] [log] [blame]
// Copyright 2017 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 maintapi
import (
"context"
"encoding/hex"
"flag"
"fmt"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/build/gerrit"
"golang.org/x/build/maintner"
"golang.org/x/build/maintner/godata"
"golang.org/x/build/maintner/maintnerd/apipb"
"grpc.go4.org"
"grpc.go4.org/codes"
)
func TestGetRef(t *testing.T) {
c := getGoData(t)
s := apiService{c}
req := &apipb.GetRefRequest{
GerritServer: "go.googlesource.com",
GerritProject: "go",
Ref: "refs/heads/master",
}
res, err := s.GetRef(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if len(res.Value) != 40 {
t.Errorf("go master ref = %q; want length 40 string", res.Value)
}
// Bogus ref
req.Ref = "NOT EXIST REF"
res, err = s.GetRef(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if len(res.Value) != 0 {
t.Errorf("go bogus ref = %q; want empty string", res.Value)
}
// Bogus project
req.GerritProject = "NOT EXIST PROJ"
_, err = s.GetRef(context.Background(), req)
if got, want := fmt.Sprint(err), "unknown gerrit project"; got != want {
t.Errorf("error for bogus project = %q; want %q", got, want)
}
}
var hitGerrit = flag.Bool("hit_gerrit", false, "query production Gerrit in TestFindTryWork")
func TestFindTryWork(t *testing.T) {
if !*hitGerrit {
t.Skip("skipping without flag --hit_gerrit")
}
c := getGoData(t)
s := apiService{c}
req := &apipb.GoFindTryWorkRequest{}
t0 := time.Now()
res, err := s.GoFindTryWork(context.Background(), req)
d0 := time.Since(t0)
if err != nil {
t.Fatal(err)
}
// Just for interactive debugging. This is using live data.
// The stable tests are in TestTryWorkItem and TestTryBotStatus.
t.Logf("Current: %v", res)
t1 := time.Now()
res, err = s.GoFindTryWork(context.Background(), req)
d1 := time.Since(t1)
t.Logf("Latency: %v, then %v", d0, d1)
t.Logf("Cached: %v, %v", res, err)
}
func TestTryBotStatus(t *testing.T) {
c := getGoData(t)
tests := []struct {
proj string
clnum int32
msgCutoff int
wantTry bool
wantDone bool
}{
{"go", 51430, 1, true, false},
{"go", 51430, 2, true, false},
{"go", 51430, 3, true, true},
{"build", 48968, 5, true, false}, // adding trybot (coordinator ignores for "build" repo)
{"build", 48968, 6, false, false}, // removing it
}
for _, tt := range tests {
cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum)
if cl == nil {
t.Errorf("CL %d in %s not found", tt.clnum, tt.proj)
continue
}
old := *cl // save before mutations
cl.Version = cl.Messages[tt.msgCutoff-1].Version
cl.Messages = cl.Messages[:tt.msgCutoff]
gotTry, gotDone := tryBotStatus(cl, false /* not staging */)
if gotTry != tt.wantTry || gotDone != tt.wantDone {
t.Errorf("tryBotStatus(%q, %d) after %d messages = try/done %v, %v; want %v, %v",
tt.proj, tt.clnum, tt.msgCutoff, gotTry, gotDone, tt.wantTry, tt.wantDone)
for _, msg := range cl.Messages {
t.Logf(" msg ver=%d, text=%q", msg.Version, msg.Message)
}
}
*cl = old // restore
}
}
func TestTryWorkItem(t *testing.T) {
c := getGoData(t)
tests := []struct {
proj string
clnum int32
ci *gerrit.ChangeInfo
comments map[string][]gerrit.CommentInfo
want string
}{
// Same Change-Id, different branch:
{"go", 51430, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"master" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"45a4609c0ae214e448612e0bc0846e2f2682f1b2" `},
{"go", 51450, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"release-branch.go1.9" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"7320506bc58d3a55eff2c67b2ec65cfa94f7b0a7" `},
// Different project:
{"build", 51432, &gerrit.ChangeInfo{}, nil, `project:"build" branch:"master" change_id:"I1f71836da7008e58d3e76e2cc3170e96cd57ddf6" commit:"9251bc9950baff61d95da0761e2e4bfab61ed210" `},
// With comments:
{
proj: "go",
clnum: 201203,
ci: &gerrit.ChangeInfo{
CurrentRevision: "f99d33e72efdea68fce39765bc94479b5ebed0a9",
Revisions: map[string]gerrit.RevisionInfo{
"f99d33e72efdea68fce39765bc94479b5ebed0a9": {PatchSetNumber: 88},
},
Messages: []gerrit.ChangeMessageInfo{
{
Author: &gerrit.AccountInfo{NumericID: 1234},
Message: "Patch Set 1: Run-TryBot+1\n\n(1 comment)",
Time: gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 27, 23, 0, time.UTC)),
RevisionNumber: 1,
},
{
Author: &gerrit.AccountInfo{NumericID: 5678},
Message: "Patch Set 2: Foo-2 Run-TryBot+1\n\n(1 comment)",
Time: gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 28, 47, 0, time.UTC)),
RevisionNumber: 2,
},
},
},
comments: map[string][]gerrit.CommentInfo{
"/PATCHSET_LEVEL": {
{
PatchSet: 1,
Message: "TRY=foo",
Updated: gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 27, 23, 0, time.UTC)),
Author: &gerrit.AccountInfo{NumericID: 1234},
},
{
PatchSet: 2,
Message: "A preceding sentence.\nTRY=bar, baz\nA following sentence.",
Updated: gerrit.TimeStamp(time.Date(2020, 7, 7, 23, 28, 47, 0, time.UTC)),
Author: &gerrit.AccountInfo{NumericID: 5678},
},
},
},
want: `project:"go" branch:"master" change_id:"I358eb7b11768df8c80fb7e805abd4cd01d52bb9b" commit:"f99d33e72efdea68fce39765bc94479b5ebed0a9" version:88 try_message:<message:"foo" author_id:1234 version:1 > try_message:<message:"bar, baz" author_id:5678 version:2 > `,
},
}
for _, tt := range tests {
cl := c.Gerrit().Project("go.googlesource.com", tt.proj).CL(tt.clnum)
if cl == nil {
t.Errorf("CL %d in %s not found", tt.clnum, tt.proj)
continue
}
got := fmt.Sprint(tryWorkItem(cl, tt.ci, tt.comments))
if got != tt.want {
t.Errorf("tryWorkItem(%q, %v) mismatch:\n got: %#q\nwant: %#q", tt.proj, tt.clnum, got, tt.want)
}
}
}
var (
corpusMu sync.Mutex
corpusCache *maintner.Corpus
)
func getGoData(tb testing.TB) *maintner.Corpus {
if testing.Short() {
tb.Skip("skipping maintner godata test in short mode")
}
corpusMu.Lock()
defer corpusMu.Unlock()
if corpusCache != nil {
return corpusCache
}
var err error
corpusCache, err = godata.Get(context.Background())
if err != nil {
tb.Fatalf("getting corpus: %v", err)
}
return corpusCache
}
func TestSupportedGoReleases(t *testing.T) {
tests := []struct {
goProj nonChangeRefLister
want []*apipb.GoRelease
}{
// A sample of real data from maintner.
{
goProj: gerritProject{
refs: []refHash{
{"HEAD", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")},
{"refs/heads/dev.boringcrypto", gitHash("13bf5b80e8d8841a2a3c9b0d5dec65a0c8636253")},
{"refs/heads/dev.boringcrypto.go1.10", gitHash("2e2a04a605b6c3fc6e733810bdcd0200d8ed25a8")},
{"refs/heads/dev.boringcrypto.go1.11", gitHash("685dc1638240af70c86a146b0ddb86d51d64f269")},
{"refs/heads/dev.typealias", gitHash("8a5ef1501dee0715093e87cdc1c9b6becb81c882")},
{"refs/heads/master", gitHash("5168fcf63f5001b38f9ac64ce5c5e3c2d397363d")},
{"refs/heads/release-branch.go1", gitHash("08b97d4061dd75ceec1d44e4335183cd791c9306")},
{"refs/heads/release-branch.go1.1", gitHash("1d6d8fca241bb611af51e265c1b5a2e9ae904702")},
{"refs/heads/release-branch.go1.10", gitHash("e97b7d68f107ff60152f5bd5701e0286f221ee93")},
{"refs/heads/release-branch.go1.11", gitHash("97781d2ed116d2cd9cb870d0b84fc0ec598c9abc")},
{"refs/heads/release-branch.go1.10-security", gitHash("25ca8f49c3fc4a68daff7a23ab613e3453be5cda")},
{"refs/heads/release-branch.go1.11-security", gitHash("90c896448691b5edb0ab11110f37234f63cd28ed")},
{"refs/heads/release-branch.go1.2", gitHash("43d00b0942c1c6f43993ac71e1eea48e62e22b8d")},
{"refs/heads/release-branch.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")},
{"refs/heads/release-branch.r60", gitHash("394b383a1ee0ac3fec5e453a7dbe590d3ce6d6b0")},
{"refs/notes/review", gitHash("c46ab9dacb2ac618d86f1c1f719bc2de46010e86")},
{"refs/tags/1.10beta1.mailed", gitHash("2df74db61620771e4f878c9e1db7aeecc00808ba")},
{"refs/tags/andybons/blog.mailed", gitHash("707a89416af909a3af6c26df93995bc17bf9ce81")},
{"refs/tags/go1", gitHash("6174b5e21e73714c63061e66efdbe180e1c5491d")},
{"refs/tags/go1.0.1", gitHash("2fffba7fe19690e038314d17a117d6b87979c89f")},
{"refs/tags/go1.0.2", gitHash("cb6c6570b73a1c4d19cad94570ed277f7dae55ac")},
{"refs/tags/go1.0.3", gitHash("30be9b4313622c2077539e68826194cb1028c691")},
{"refs/tags/go1.1", gitHash("205f850ceacfc39d1e9d76a9569416284594ce8c")},
{"refs/tags/go1.10", gitHash("bf86aec25972f3a100c3aa58a6abcbcc35bdea49")},
{"refs/tags/go1.10.1", gitHash("ac7c0ee26dda18076d5f6c151d8f920b43340ae3")},
{"refs/tags/go1.10.2", gitHash("71bdbf431b79dff61944f22c25c7e085ccfc25d5")},
{"refs/tags/go1.10.3", gitHash("fe8a0d12b14108cbe2408b417afcaab722b0727c")},
{"refs/tags/go1.10.4", gitHash("2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78")},
{"refs/tags/go1.10beta1", gitHash("9ce6b5c2ed5d3d5251b9a6a0c548d5fb2c8567e8")},
{"refs/tags/go1.10beta2", gitHash("594668a5a96267a46282ce3007a584ec07adf705")},
{"refs/tags/go1.10rc1", gitHash("5348aed83e39bd1d450d92d7f627e994c2db6ebf")},
{"refs/tags/go1.10rc2", gitHash("20e228f2fdb44350c858de941dff4aea9f3127b8")},
{"refs/tags/go1.11", gitHash("41e62b8c49d21659b48a95216e3062032285250f")},
{"refs/tags/go1.11.1", gitHash("26957168c4c0cdcc7ca4f0b19d0eb19474d224ac")},
{"refs/tags/go1.11beta1", gitHash("a12c1f26e4cc602dae62ec065a237172a5b8f926")},
{"refs/tags/go1.11beta2", gitHash("c814ac44c0571f844718f07aa52afa47e37fb1ed")},
{"refs/tags/go1.11beta3", gitHash("1b870077c896379c066b41657d3c9062097a6943")},
{"refs/tags/go1.11rc1", gitHash("807e7f2420c683384dc9c6db498808ba1b7aab17")},
{"refs/tags/go1.11rc2", gitHash("02c0c32960f65d0b9c66ec840c612f5f9623dc51")},
{"refs/tags/go1.9.7", gitHash("7df09b4a03f9e53334672674ba7983d5e7128646")},
{"refs/tags/go1.9beta1", gitHash("952ecbe0a27aadd184ca3e2c342beb464d6b1653")},
{"refs/tags/go1.9beta2", gitHash("eab99a8d548f8ba864647ab171a44f0a5376a6b3")},
{"refs/tags/go1.9rc1", gitHash("65c6c88a9442b91d8b2fd0230337b1fda4bb6cdf")},
{"refs/tags/go1.9rc2", gitHash("048c9cfaacb6fe7ac342b0acd8ca8322b6c49508")},
{"refs/tags/release.r59", gitHash("5d9765785dff74784bbdad43f7847b6825509032")},
{"refs/tags/release.r60", gitHash("5464bfebe723752dfc09a6dd6b361b8e79db5995")},
{"refs/tags/release.r60.1", gitHash("4af7136fcf874e212d66c72178a68db969918b25")},
{"refs/tags/weekly", gitHash("3895b5051df256b442d0b0af50debfffd8d75164")},
{"refs/tags/weekly.2009-11-10", gitHash("78c47c36b2984058c1bec0bd72e0b127b24fcd44")},
{"refs/tags/weekly.2009-11-10.1", gitHash("c57054f7b49539ca4ed6533267c1c20c39aaaaa5")},
},
},
want: []*apipb.GoRelease{
{
Major: 1, Minor: 11, Patch: 1,
TagName: "go1.11.1",
TagCommit: "26957168c4c0cdcc7ca4f0b19d0eb19474d224ac",
BranchName: "release-branch.go1.11",
BranchCommit: "97781d2ed116d2cd9cb870d0b84fc0ec598c9abc",
},
{
Major: 1, Minor: 10, Patch: 4,
TagName: "go1.10.4",
TagCommit: "2191fce26a7fd1cd5b4975e7bd44ab44b1d9dd78",
BranchName: "release-branch.go1.10",
BranchCommit: "e97b7d68f107ff60152f5bd5701e0286f221ee93",
},
},
},
// Detect and handle a new major version.
{
goProj: gerritProject{
refs: []refHash{
{"refs/tags/go1.5", gitHash("9b82ca331d1fa30e3428e7914ba780ae7f75a702")},
{"refs/tags/go1.42.1", gitHash("23982c09ae5ac811d1dd0099e1626596ade61000")},
{"refs/tags/go1", gitHash("5c503fde0aa534d3259533802052f936c95fa782")},
{"refs/tags/go2", gitHash("43126518de2eb0dadc0917a593f08637318986bf")},
{"refs/tags/go1.11.111", gitHash("c59f000d9bb66592ff84a942014afd1a7be4c953")}, // The onesiest release ever!
{"refs/heads/release-branch.go1", gitHash("b0f2d801c19fc8798ecf67e50364a44dba606fcd")},
{"refs/heads/release-branch.go1.5", gitHash("a6ae58c93408bcc17758d397eed0ace973de8481")},
{"refs/heads/release-branch.go1.11", gitHash("f4f148ef7962271ff8ffcebf13400ded535e9957")},
{"refs/heads/release-branch.go1.42", gitHash("362986e7a4b5edc911ed55324c37106c40abe3fb")},
{"refs/heads/release-branch.go2", gitHash("cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59")},
{"refs/heads/release-branch.go1.2", gitHash("6523e1eb33ef792df04e08462ed332b95311261e")},
// It doesn't count as a release if there's no corresponding release-branch.go1.43 release branch.
{"refs/tags/go1.43", gitHash("3aa7f7065ecf717b1dd6512bb7a9f40625fc8cb5")},
},
},
want: []*apipb.GoRelease{
{
Major: 2, Minor: 0, Patch: 0,
TagName: "go2",
TagCommit: "43126518de2eb0dadc0917a593f08637318986bf",
BranchName: "release-branch.go2",
BranchCommit: "cfbe0f14bcbf1e773f8dd9a968c80cf0b9238c59",
},
{
Major: 1, Minor: 42, Patch: 1,
TagName: "go1.42.1",
TagCommit: "23982c09ae5ac811d1dd0099e1626596ade61000",
BranchName: "release-branch.go1.42",
BranchCommit: "362986e7a4b5edc911ed55324c37106c40abe3fb",
},
},
},
}
for i, tt := range tests {
got, err := supportedGoReleases(tt.goProj)
if err != nil {
t.Fatalf("%d: supportedGoReleases: %v", i, err)
}
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("%d: supportedGoReleases: (-got +want)\n%s", i, diff)
}
}
}
func TestGetDashboard(t *testing.T) {
c := getGoData(t)
s := apiService{c}
type check func(t *testing.T, res *apipb.DashboardResponse, resErr error)
var noError check = func(t *testing.T, res *apipb.DashboardResponse, resErr error) {
t.Helper()
if resErr != nil {
t.Fatalf("GetDashboard: %v", resErr)
}
}
var commitsTruncated check = func(t *testing.T, res *apipb.DashboardResponse, _ error) {
t.Helper()
if !res.CommitsTruncated {
t.Errorf("CommitsTruncated = false; want true")
}
if len(res.Commits) == 0 {
t.Errorf("no commits; expected some commits when expecting CommitsTruncated")
}
}
hasBranch := func(branch string) check {
return func(t *testing.T, res *apipb.DashboardResponse, _ error) {
ok := false
for _, b := range res.Branches {
if b == branch {
ok = true
break
}
}
if !ok {
t.Errorf("didn't find expected branch %q; got branches: %q", branch, res.Branches)
}
}
}
hasRepoHead := func(proj string) check {
return func(t *testing.T, res *apipb.DashboardResponse, _ error) {
ok := false
var got []string
for _, rh := range res.RepoHeads {
if rh.GerritProject == proj {
ok = true
}
got = append(got, rh.GerritProject)
}
if !ok {
t.Errorf("didn't find expected repo head %q; got: %q", proj, got)
}
}
}
var hasThreeReleases check = func(t *testing.T, res *apipb.DashboardResponse, _ error) {
t.Helper()
var got []string
var gotMaster int
var gotReleaseBranch int
var uniq = map[string]bool{}
for _, r := range res.Releases {
got = append(got, r.BranchName)
uniq[r.BranchName] = true
if r.BranchName == "master" {
gotMaster++
}
if strings.HasPrefix(r.BranchName, "release-branch.go") {
gotReleaseBranch++
}
}
if len(uniq) != 3 {
t.Errorf("expected 3 Go releases, got: %q", got)
}
if gotMaster != 1 {
t.Errorf("expected 1 Go release to be master, got: %q", got)
}
if gotReleaseBranch != 2 {
t.Errorf("expected 2 Go releases to be release branches, got: %q", got)
}
}
wantRPCError := func(code codes.Code) check {
return func(t *testing.T, _ *apipb.DashboardResponse, err error) {
if grpc.Code(err) != code {
t.Errorf("expected RPC code %v; got %v (err %v)", code, grpc.Code(err), err)
}
}
}
basicChecks := []check{
noError,
commitsTruncated,
hasBranch("master"),
hasBranch("release-branch.go1.4"),
hasBranch("release-branch.go1.13"),
hasRepoHead("net"),
hasRepoHead("sys"),
hasThreeReleases,
}
tests := []struct {
name string
req *apipb.DashboardRequest
checks []check
}{
// Verify that the default view (with no options) works.
{
name: "zero_value",
req: &apipb.DashboardRequest{},
checks: basicChecks,
},
// Or with explicit values:
{
name: "zero_value_effectively",
req: &apipb.DashboardRequest{
Repo: "go",
Branch: "master",
},
checks: basicChecks,
},
// Max commits:
{
name: "max_commits",
req: &apipb.DashboardRequest{MaxCommits: 1},
checks: []check{
noError,
commitsTruncated,
func(t *testing.T, res *apipb.DashboardResponse, _ error) {
if got, want := len(res.Commits), 1; got != want {
t.Errorf("got %v commits; want %v", got, want)
}
},
},
},
// Verify that branch=mixed doesn't return an error at least.
{
name: "mixed",
req: &apipb.DashboardRequest{Branch: "mixed"},
checks: []check{
noError,
commitsTruncated,
hasRepoHead("sys"),
hasThreeReleases,
},
},
// Verify non-Go repos:
{
name: "non_go_repo",
req: &apipb.DashboardRequest{Repo: "golang.org/x/net"},
checks: []check{
noError,
commitsTruncated,
func(t *testing.T, res *apipb.DashboardResponse, _ error) {
for _, c := range res.Commits {
if c.GoCommitAtTime == "" {
t.Errorf("response contains commit without GoCommitAtTime")
}
if c.GoCommitLatest == "" {
t.Errorf("response contains commit without GoCommitLatest")
}
if t.Failed() {
return
}
}
},
},
},
// Validate rejection of bad requests:
{
name: "bad-repo",
req: &apipb.DashboardRequest{Repo: "NOT_EXIST"},
checks: []check{wantRPCError(codes.NotFound)},
},
{
name: "bad-branch",
req: &apipb.DashboardRequest{Branch: "NOT_EXIST"},
checks: []check{wantRPCError(codes.NotFound)},
},
{
name: "mixed-with-pagination",
req: &apipb.DashboardRequest{Branch: "mixed", Page: 5},
checks: []check{wantRPCError(codes.InvalidArgument)},
},
{
name: "negative-page",
req: &apipb.DashboardRequest{Page: -1},
checks: []check{wantRPCError(codes.InvalidArgument)},
},
{
name: "too-big-page",
req: &apipb.DashboardRequest{Page: 1e6},
checks: []check{wantRPCError(codes.InvalidArgument)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := s.GetDashboard(context.Background(), tt.req)
for _, c := range tt.checks {
c(t, res, err)
}
})
}
}
type gerritProject struct {
refs []refHash
}
func (gp gerritProject) ForeachNonChangeRef(fn func(ref string, hash maintner.GitHash) error) error {
for _, r := range gp.refs {
err := fn(r.Ref, r.Hash)
if err != nil {
return err
}
}
return nil
}
type refHash struct {
Ref string
Hash maintner.GitHash
}
func gitHash(hexa string) maintner.GitHash {
if len(hexa) != 40 {
panic(fmt.Errorf("bogus git hash %q", hexa))
}
binary, err := hex.DecodeString(hexa)
if err != nil {
panic(fmt.Errorf("bogus git hash %q: %v", hexa, err))
}
return maintner.GitHash(binary)
}