blob: 41c650fba24c3f80c9d66d1132ed6cce7a24eea1 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gerritfake
import (
"context"
"fmt"
"sort"
"testing"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/timestamppb"
gerritutil "go.chromium.org/luci/common/api/gerrit"
"go.chromium.org/luci/common/clock/testclock"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/cv/internal/gerrit"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestRelationship(t *testing.T) {
t.Parallel()
Convey("Relationship works", t, func() {
ci1 := CI(1, PS(1), AllRevs())
ci2 := CI(2, PS(2), AllRevs())
ci3 := CI(3, PS(3), AllRevs())
ci4 := CI(4, PS(4), AllRevs())
f := WithCIs("host", ACLRestricted("infra"), ci1, ci2, ci3, ci4)
// Diamond using latest patchsets.
// --<-- 2_2 --<--
// / \
// 1_1 4_4
// \ /
// --<-- 3-3 --<--
f.SetDependsOn("host", ci4, ci3, ci2) // 2 parents.
f.SetDependsOn("host", ci3, ci1)
f.SetDependsOn("host", ci2, ci1)
// Chain made by prior patchsets.
// 2_1 --<-- 3_2 --<-- 4_3
f.SetDependsOn("host", "4_3", "3_2")
f.SetDependsOn("host", "3_2", "2_1")
ctx := context.Background()
Convey("with allowed project", func() {
gc, err := f.MakeClient(ctx, "host", "infra")
So(err, ShouldBeNil)
Convey("No relations", func() {
resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
Number: 4,
Project: "infra/infra",
RevisionId: "1",
})
So(err, ShouldBeNil)
So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{})
})
Convey("Descendants only", func() {
resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
Number: 2,
Project: "infra/infra",
RevisionId: "1",
})
So(err, ShouldBeNil)
sortRelated(resp)
So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000002-001",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
},
Number: 2,
Patchset: 1,
CurrentPatchset: 2,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000003-002",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000002-001"}},
},
Number: 3,
Patchset: 2,
CurrentPatchset: 3,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000004-003",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000003-002"}},
},
Number: 4,
Patchset: 3,
CurrentPatchset: 4,
},
},
})
})
Convey("Diamond", func() {
resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
Number: 4,
RevisionId: "4",
})
So(err, ShouldBeNil)
sortRelated(resp)
So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000001-001",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
},
Number: 1,
Patchset: 1,
CurrentPatchset: 1,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000002-002",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
},
Number: 2,
Patchset: 2,
CurrentPatchset: 2,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000003-003",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
},
Number: 3,
Patchset: 3,
CurrentPatchset: 3,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000004-004",
Parents: []*gerritpb.CommitInfo_Parent{
{Id: "rev-000003-003"},
{Id: "rev-000002-002"},
},
},
Number: 4,
Patchset: 4,
CurrentPatchset: 4,
},
},
})
})
Convey("Part of Diamond", func() {
resp, err := gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
Number: 3,
RevisionId: "3",
})
So(err, ShouldBeNil)
sortRelated(resp)
So(resp, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{
Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000001-001",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "fake_parent_commit"}},
},
Number: 1,
Patchset: 1,
CurrentPatchset: 1,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000003-003",
Parents: []*gerritpb.CommitInfo_Parent{{Id: "rev-000001-001"}},
},
Number: 3,
Patchset: 3,
CurrentPatchset: 3,
},
{
Project: "infra/infra",
Commit: &gerritpb.CommitInfo{
Id: "rev-000004-004",
Parents: []*gerritpb.CommitInfo_Parent{
{Id: "rev-000003-003"},
{Id: "rev-000002-002"},
},
},
Number: 4,
Patchset: 4,
CurrentPatchset: 4,
},
},
})
})
})
Convey("with disallowed project", func() {
gc, err := f.MakeClient(ctx, "host", "spying-luci-project")
So(err, ShouldBeNil)
_, err = gc.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{
Number: 4,
RevisionId: "1",
})
So(err, ShouldNotBeNil)
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
})
}
// sortRelated ensures deterministic yet ultimately abitrary order.
func sortRelated(r *gerritpb.GetRelatedChangesResponse) {
key := func(i int) string {
c := r.GetChanges()[i]
return fmt.Sprintf("%40s:%020d:%020d", c.GetCommit().GetId(), c.GetNumber(), c.GetPatchset())
}
sort.Slice(r.GetChanges(), func(i, j int) bool { return key(i) < key(j) })
}
func TestFiles(t *testing.T) {
t.Parallel()
Convey("Files' handling works", t, func() {
sortedFiles := func(r *gerritpb.ListFilesResponse) []string {
fs := make([]string, 0, len(r.GetFiles()))
for f := range r.GetFiles() {
fs = append(fs, f)
}
sort.Strings(fs)
return fs
}
ciDefault := CI(1)
ciCustom := CI(2, Files("ps1/cus.tom", "bl.ah"), PS(2), Files("still/custom"))
ciNoFiles := CI(3, Files())
f := WithCIs("host", ACLRestricted("infra"), ciDefault, ciCustom, ciNoFiles)
ctx := context.Background()
gc, err := f.MakeClient(ctx, "host", "infra")
So(err, ShouldBeNil)
Convey("change or revision NotFound", func() {
_, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{Number: 123213, RevisionId: "1"})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
_, err = gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
Number: ciDefault.GetNumber(),
RevisionId: "not existing",
})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
Convey("Default", func() {
resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
Number: ciDefault.GetNumber(),
RevisionId: ciDefault.GetCurrentRevision(),
})
So(err, ShouldBeNil)
So(sortedFiles(resp), ShouldResemble, []string{"ps001/c.cpp", "shared/s.py"})
})
Convey("Custom", func() {
resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
Number: ciCustom.GetNumber(),
RevisionId: "1",
})
So(err, ShouldBeNil)
So(sortedFiles(resp), ShouldResemble, []string{"bl.ah", "ps1/cus.tom"})
resp, err = gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
Number: ciCustom.GetNumber(),
RevisionId: "2",
})
So(err, ShouldBeNil)
So(sortedFiles(resp), ShouldResemble, []string{"still/custom"})
})
Convey("NoFiles", func() {
resp, err := gc.ListFiles(ctx, &gerritpb.ListFilesRequest{
Number: ciNoFiles.GetNumber(),
RevisionId: ciNoFiles.GetCurrentRevision(),
})
So(err, ShouldBeNil)
So(resp.GetFiles(), ShouldHaveLength, 0)
})
})
}
func TestGetChange(t *testing.T) {
t.Parallel()
Convey("GetChange handling works", t, func() {
ci := CI(100100, PS(4), AllRevs())
So(ci.GetRevisions(), ShouldHaveLength, 4)
f := WithCIs("host", ACLRestricted("infra"), ci)
ctx := context.Background()
gc, err := f.MakeClient(ctx, "host", "infra")
So(err, ShouldBeNil)
Convey("NotFound", func() {
_, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{Number: 12321})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
Convey("Default", func() {
resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{Number: 100100})
So(err, ShouldBeNil)
So(resp.GetCurrentRevision(), ShouldEqual, "")
So(resp.GetRevisions(), ShouldHaveLength, 0)
So(resp.GetLabels(), ShouldHaveLength, 0)
})
Convey("CURRENT_REVISION", func() {
resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{
Number: 100100,
Options: []gerritpb.QueryOption{gerritpb.QueryOption_CURRENT_REVISION}})
So(err, ShouldBeNil)
So(resp.GetRevisions(), ShouldHaveLength, 1)
So(resp.GetRevisions()[resp.GetCurrentRevision()], ShouldNotBeNil)
})
Convey("Full", func() {
resp, err := gc.GetChange(ctx, &gerritpb.GetChangeRequest{
Number: 100100,
Options: []gerritpb.QueryOption{
gerritpb.QueryOption_ALL_REVISIONS,
gerritpb.QueryOption_DETAILED_ACCOUNTS,
gerritpb.QueryOption_DETAILED_LABELS,
gerritpb.QueryOption_SKIP_MERGEABLE,
gerritpb.QueryOption_MESSAGES,
gerritpb.QueryOption_SUBMITTABLE,
}})
So(err, ShouldBeNil)
So(resp, ShouldResembleProto, ci)
})
})
}
func TestListChanges(t *testing.T) {
t.Parallel()
Convey("ListChanges works", t, func() {
f := WithCIs("empty", ACLRestricted("empty"))
ctx := context.Background()
mustCurrentClient := func(host, luciProject string) gerrit.Client {
cl, err := f.MakeClient(ctx, host, luciProject)
So(err, ShouldBeNil)
return cl
}
listChangeIDs := func(client gerrit.Client, req *gerritpb.ListChangesRequest) []int {
out, err := client.ListChanges(ctx, req)
So(err, ShouldBeNil)
So(out.GetMoreChanges(), ShouldBeFalse)
ids := make([]int, len(out.GetChanges()))
for i, ch := range out.GetChanges() {
ids[i] = int(ch.GetNumber())
if i > 0 {
// Ensure monotonically non-decreasing update timestamps.
prior := out.GetChanges()[i-1]
So(prior.GetUpdated().AsTime().Before(ch.GetUpdated().AsTime()), ShouldBeFalse)
}
}
return ids
}
f.AddFrom(WithCIs("chrome-internal", ACLRestricted("infra-internal"),
CI(9001, Project("infra/infra-internal")),
CI(9002, Project("infra/infra-internal")),
))
Convey("ACLs enforced", func() {
So(listChangeIDs(mustCurrentClient("chrome-internal", "spy"),
&gerritpb.ListChangesRequest{}), ShouldResemble, []int{})
So(listChangeIDs(mustCurrentClient("chrome-internal", "infra-internal"),
&gerritpb.ListChangesRequest{}), ShouldResemble, []int{9002, 9001})
})
var epoch = time.Date(2011, time.February, 3, 4, 5, 6, 7, time.UTC)
u0 := Updated(epoch)
u1 := Updated(epoch.Add(time.Minute))
u2 := Updated(epoch.Add(2 * time.Minute))
f.AddFrom(WithCIs("chromium", ACLPublic(),
CI(8001, u1, Project("infra/infra"), CQ(+2)),
CI(8002, u2, Project("infra/luci/luci-go"), Vote("Commit-Queue", +1), Vote("Code-Review", -1)),
CI(8003, u0, Project("infra/luci/luci-go"), Status("MERGED"), Vote("Code-Review", +1)),
))
Convey("Order and limit", func() {
g := mustCurrentClient("chromium", "anyone")
So(listChangeIDs(g, &gerritpb.ListChangesRequest{}), ShouldResemble, []int{8002, 8001, 8003})
out, err := g.ListChanges(ctx, &gerritpb.ListChangesRequest{Limit: 2})
So(err, ShouldBeNil)
So(out.GetMoreChanges(), ShouldBeTrue)
So(out.GetChanges()[0].GetNumber(), ShouldEqual, 8002)
So(out.GetChanges()[1].GetNumber(), ShouldEqual, 8001)
})
Convey("Filtering works", func() {
query := func(q string) []int {
return listChangeIDs(mustCurrentClient("chromium", "anyone"),
&gerritpb.ListChangesRequest{Query: q})
}
Convey("before/after", func() {
So(gerritutil.FormatTime(epoch), ShouldResemble, `"2011-02-03 04:05:06.000000007"`)
So(query(`before:"2011-02-03 04:05:06.000000006"`), ShouldResemble, []int{})
// 1 ns later
So(query(`before:"2011-02-03 04:05:06.000000007"`), ShouldResemble, []int{8003})
So(query(` after:"2011-02-03 04:05:06.000000007"`), ShouldResemble, []int{8002, 8001, 8003})
// 1 minute later
So(query(` after:"2011-02-03 04:06:06.000000007"`), ShouldResemble, []int{8002, 8001})
// 1 minute later
So(query(` after:"2011-02-03 04:07:06.000000007"`), ShouldResemble, []int{8002})
// Surround middle CL:
So(query(``+
` after:"2011-02-03 04:05:30.000000000" `+
`before:"2011-02-03 04:06:30.000000000"`), ShouldResemble, []int{8001})
})
Convey("Project prefix", func() {
So(query(`projects:"inf"`), ShouldResemble, []int{8002, 8001, 8003})
So(query(`projects:"infra/"`), ShouldResemble, []int{8002, 8001, 8003})
So(query(`projects:"infra/luci"`), ShouldResemble, []int{8002, 8003})
So(query(`projects:"typo"`), ShouldResemble, []int{})
})
Convey("Project exact", func() {
So(query(`project:"infra/infra"`), ShouldResemble, []int{8001})
So(query(`project:"infra"`), ShouldResemble, []int{})
So(query(`(project:"infra/infra" OR project:"infra/luci/luci-go")`), ShouldResemble,
[]int{8002, 8001, 8003})
})
Convey("Status", func() {
So(query(`status:new`), ShouldResemble, []int{8002, 8001})
So(query(`status:abandoned`), ShouldResemble, []int{})
So(query(`status:merged`), ShouldResemble, []int{8003})
})
Convey("label", func() {
So(query(`label:Commit-Queue>0`), ShouldResemble, []int{8002, 8001})
So(query(`label:Commit-Queue>1`), ShouldResemble, []int{8001})
So(query(`label:Code-Review>-1`), ShouldResemble, []int{8003})
})
Convey("Typical CV query", func() {
So(query(`label:Commit-Queue>0 status:NEW project:"infra/infra"`),
ShouldResemble, []int{8001})
So(query(`label:Commit-Queue>0 status:NEW projects:"infra"`),
ShouldResemble, []int{8002, 8001})
So(query(`label:Commit-Queue>0 status:NEW projects:"infra"`+
` after:"2011-02-03 04:06:30.000000000" `+
`before:"2011-02-03 04:08:30.000000000"`), ShouldResemble, []int{8002})
So(query(`label:Commit-Queue>0 status:NEW `+
`(project:"infra" OR project:"infra/luci/luci-go")`+
` after:"2011-02-03 04:06:30.000000000" `+
`before:"2011-02-03 04:08:30.000000000"`), ShouldResemble, []int{8002})
})
})
Convey("Bad queries", func() {
test := func(query string) error {
client, err := f.MakeClient(ctx, "infra", "chromium")
So(err, ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{Query: query})
So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument)
So(err, ShouldErrLike, `invalid query argument`)
return err
}
So(test(`"unmatched quote`), ShouldErrLike, `invalid query argument "\"unmatched quote"`)
So(test(`status:new "unmatched`), ShouldErrLike, `unrecognized token "\"unmatched`)
So(test(`project:"unmatched`), ShouldErrLike, `"project:\"unmatched": expected quoted string`)
So(test(`project:raw/not/supported`), ShouldErrLike, `expected quoted string`)
So(test(`project:"one" OR project:"two"`), ShouldErrLike, `"OR" must be inside ()`)
So(test(`project:"one" project:"two")`), ShouldErrLike, `"project:" must be inside ()`)
// This error can be better, but UX isn't essential for a fake.
So(test(`(project:"one" OR`), ShouldErrLike, `"" must be outside of ()`)
So(test(`status:rand-om`), ShouldErrLike, `unrecognized status "rand-om"`)
So(test(`status:0`), ShouldErrLike, `unrecognized status "0"`)
So(test(`label:0`), ShouldErrLike, `invalid label: 0`)
So(test(`label:Commit-Queue`), ShouldErrLike, `invalid label: Commit-Queue`)
// Note these are actually allowed in Gerrit.
So(test(`label:Commit-Queue<1`), ShouldErrLike, `invalid label: Commit-Queue<1`)
So(test(`before:2019-20-01`), ShouldErrLike, `failed to parse Gerrit timestamp "2019-20-01"`)
So(test(` after:2019-20-01`), ShouldErrLike, `failed to parse Gerrit timestamp "2019-20-01"`)
So(test(`before:"2019-20-01"`), ShouldErrLike, `failed to parse Gerrit timestamp "\"2019-20-01\""`)
})
})
}
func TestSetReview(t *testing.T) {
t.Parallel()
Convey("SetReview", t, func() {
ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
user := U("user-123")
accountID := user.AccountId
before := tc.Now().Add(-1 * time.Minute)
ciBefore := CI(10001, CQ(1, before, user), Updated(before))
f := WithCIs(
"example",
ACLGrant(OpReview, codes.PermissionDenied, "chromium").Or(ACLGrant(OpAlterVotesOfOthers, codes.PermissionDenied, "chromium")),
ciBefore,
)
tc.Add(2 * time.Minute)
mustWriterClient := func(host, luciProject string) gerrit.Client {
cl, err := f.MakeClient(ctx, host, luciProject)
So(err, ShouldBeNil)
return cl
}
latestCI := func() *gerritpb.ChangeInfo {
return f.GetChange("example", 10001).Info
}
Convey("ACLs enforced", func() {
client := mustWriterClient("example", "not-chromium")
res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 11111,
})
So(res, ShouldBeNil)
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Message: "this is a message",
})
So(res, ShouldBeNil)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Labels: map[string]int32{
"Commit-Queue": 0,
},
})
So(res, ShouldBeNil)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
res, err = client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Labels: map[string]int32{
"Commit-Queue": 0,
},
OnBehalfOf: accountID,
})
So(res, ShouldBeNil)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("Post message", func() {
client := mustWriterClient("example", "chromium")
res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Message: "this is a message",
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ReviewResult{})
So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
So(latestCI().GetMessages(), ShouldResembleProto, []*gerritpb.ChangeMessageInfo{
{
Id: "0",
Author: U("chromium"),
Date: timestamppb.New(tc.Now()),
Message: "this is a message",
},
})
})
Convey("Set vote", func() {
client := mustWriterClient("example", "chromium")
res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Labels: map[string]int32{
"Commit-Queue": 2,
},
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ReviewResult{
Labels: map[string]int32{
"Commit-Queue": 2,
},
})
So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
So(latestCI().GetLabels()["Commit-Queue"].GetAll(), ShouldResembleProto, []*gerritpb.ApprovalInfo{
{
User: user,
Value: 1,
Date: timestamppb.New(before),
},
{
User: U("chromium"),
Value: 2,
Date: timestamppb.New(tc.Now()),
},
})
})
Convey("Set vote on behalf of", func() {
client := mustWriterClient("example", "chromium")
Convey("existing voter", func() {
res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Labels: map[string]int32{
"Commit-Queue": 0,
},
OnBehalfOf: 123,
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ReviewResult{
Labels: map[string]int32{
"Commit-Queue": 0,
},
})
So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
So(NonZeroVotes(latestCI(), "Commit-Queue"), ShouldBeEmpty)
})
Convey("new voter", func() {
res, err := client.SetReview(ctx, &gerritpb.SetReviewRequest{
Number: 10001,
Labels: map[string]int32{
"Commit-Queue": 1,
},
OnBehalfOf: 789,
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ReviewResult{
Labels: map[string]int32{
"Commit-Queue": 1,
},
})
So(latestCI().GetUpdated().AsTime(), ShouldHappenAfter, ciBefore.GetUpdated().AsTime())
So(latestCI().GetLabels()["Commit-Queue"].GetAll(), ShouldResembleProto, []*gerritpb.ApprovalInfo{
{
User: user,
Value: 1,
Date: timestamppb.New(before),
},
{
User: U("user-789"),
Value: 1,
Date: timestamppb.New(tc.Now()),
},
})
})
})
})
}
func TestSubmitRevision(t *testing.T) {
t.Parallel()
Convey("SubmitRevision", t, func() {
const gHost = "example.com"
ctx, tc := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
var (
ciSingular = CI(10001, Updated(tc.Now()), PS(3), AllRevs())
ciStackBase = CI(20001, Updated(tc.Now()), PS(1), AllRevs())
ciStackMid = CI(20002, Updated(tc.Now()), PS(1), AllRevs())
ciStackTop = CI(20003, Updated(tc.Now()), PS(1), AllRevs())
)
f := WithCIs(
"example.com",
ACLGrant(OpSubmit, codes.PermissionDenied, "chromium"),
ciSingular, ciStackBase, ciStackMid, ciStackTop,
)
f.SetDependsOn(gHost, ciStackMid, ciStackBase)
f.SetDependsOn(gHost, ciStackTop, ciStackMid)
tc.Add(2 * time.Minute)
assertStatus := func(s gerritpb.ChangeStatus, cis ...*gerritpb.ChangeInfo) {
for _, ci := range cis {
latestCI := f.GetChange(gHost, int(ci.GetNumber())).Info
So(latestCI.GetStatus(), ShouldEqual, s)
}
}
mustWriterClient := func(host, luciProject string) gerrit.Client {
cl, err := f.MakeClient(ctx, host, luciProject)
So(err, ShouldBeNil)
return cl
}
Convey("ACLs enforced", func() {
client := mustWriterClient(gHost, "not-chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: ciSingular.GetCurrentRevision(),
})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
assertStatus(gerritpb.ChangeStatus_NEW, ciSingular)
})
Convey("ACLs enforced with CL stack", func() {
f.MutateChange(gHost, int(ciStackBase.GetNumber()), func(c *Change) {
c.ACLs = ACLGrant(OpSubmit, codes.PermissionDenied, "pretty-much-denied-to-everyone")
})
client := mustWriterClient(gHost, "chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciStackTop.GetNumber(),
RevisionId: ciStackTop.GetCurrentRevision(),
})
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
assertStatus(gerritpb.ChangeStatus_NEW, ciStackBase, ciStackMid, ciStackTop)
})
Convey("Non-existent revision", func() {
client := mustWriterClient(gHost, "chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: "non-existent",
})
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
assertStatus(gerritpb.ChangeStatus_NEW, ciSingular)
})
Convey("Old revision", func() {
client := mustWriterClient(gHost, "chromium")
var oldRev string
for rev := range ciSingular.GetRevisions() {
if rev != ciSingular.GetCurrentRevision() {
oldRev = rev
break
}
}
So(oldRev, ShouldNotBeEmpty)
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: oldRev,
})
So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
So(err, ShouldErrLike, "is not current")
})
Convey("Works", func() {
client := mustWriterClient(gHost, "chromium")
res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: ciSingular.GetCurrentRevision(),
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
Status: gerritpb.ChangeStatus_MERGED,
})
assertStatus(gerritpb.ChangeStatus_MERGED, ciSingular)
})
Convey("Works with CL stack", func() {
client := mustWriterClient(gHost, "chromium")
res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciStackTop.GetNumber(),
RevisionId: ciStackTop.GetCurrentRevision(),
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
Status: gerritpb.ChangeStatus_MERGED,
})
assertStatus(gerritpb.ChangeStatus_MERGED, ciStackBase, ciStackMid, ciStackTop)
})
Convey("Already Merged", func() {
f.MutateChange(gHost, int(ciSingular.GetNumber()), func(c *Change) {
c.Info.Status = gerritpb.ChangeStatus_MERGED
})
client := mustWriterClient(gHost, "chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: ciSingular.GetCurrentRevision(),
})
So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
So(err, ShouldErrLike, "change is merged")
})
Convey("already merged ones are skipped inside a CL Stack", func() {
verify := func() {
client := mustWriterClient(gHost, "chromium")
res, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciStackTop.GetNumber(),
RevisionId: ciStackTop.GetCurrentRevision(),
})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.SubmitInfo{
Status: gerritpb.ChangeStatus_MERGED,
})
}
Convey("base of stack", func() {
f.MutateChange(gHost, int(ciStackBase.GetNumber()), func(c *Change) {
c.Info.Status = gerritpb.ChangeStatus_MERGED
})
verify()
assertStatus(gerritpb.ChangeStatus_MERGED, ciStackBase, ciStackMid, ciStackTop)
})
Convey("mid-stack", func() {
// May happen if the mid-CL was at some point re-based on top of
// something other than ciStackBase and then submitted.
f.MutateChange(gHost, int(ciStackMid.GetNumber()), func(c *Change) {
c.Info.Status = gerritpb.ChangeStatus_MERGED
PS(int(c.Info.GetRevisions()[c.Info.GetCurrentRevision()].GetNumber() + 1))(c.Info)
})
verify()
assertStatus(gerritpb.ChangeStatus_MERGED, ciStackMid, ciStackTop)
assertStatus(gerritpb.ChangeStatus_NEW, ciStackBase)
})
})
Convey("Abandoned", func() {
f.MutateChange(gHost, int(ciSingular.GetNumber()), func(c *Change) {
c.Info.Status = gerritpb.ChangeStatus_ABANDONED
})
client := mustWriterClient(gHost, "chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciSingular.GetNumber(),
RevisionId: ciSingular.GetCurrentRevision(),
})
So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
So(err, ShouldErrLike, "change is abandoned")
})
Convey("Abandoned inside CL stack", func() {
f.MutateChange(gHost, int(ciStackMid.GetNumber()), func(c *Change) {
c.Info.Status = gerritpb.ChangeStatus_ABANDONED
})
client := mustWriterClient(gHost, "chromium")
_, err := client.SubmitRevision(ctx, &gerritpb.SubmitRevisionRequest{
Number: ciStackTop.GetNumber(),
RevisionId: ciStackTop.GetCurrentRevision(),
})
So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition)
So(err, ShouldErrLike, "change is abandoned")
})
})
}