blob: b989fef9191fc4dd4cf244d47fcd0d43ea4b474c [file] [log] [blame]
// Copyright 2022 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 rpc
import (
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
apiv0pb "go.chromium.org/luci/cv/api/v0"
"go.chromium.org/luci/cv/internal/acls"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/run"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestSearchRuns(t *testing.T) {
t.Parallel()
Convey("SearchRuns", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
srv := RunsServer{}
const projectName = "prj"
prjcfgtest.Create(ctx, projectName, &cfgpb.Config{
// TODO(crbug/1233963): remove once non-legacy ACLs are implemented.
CqStatusHost: "chromium-cq-status.appspot.com",
ConfigGroups: []*cfgpb.ConfigGroup{{Name: "first"}},
})
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:admin@example.com",
IdentityGroups: []string{acls.V0APIAllowGroup},
})
Convey("without access", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "anonymous:anonymous",
})
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
})
So(err, ShouldBeRPCPermissionDenied)
})
Convey("with no predicate", func() {
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{})
So(err, ShouldBeRPCInvalidArgument)
_, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{PageSize: 50})
So(err, ShouldBeRPCInvalidArgument)
})
Convey("with a page size that is too large", func() {
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{PageSize: maxPageSize + 1})
So(err, ShouldBeRPCInvalidArgument)
})
Convey("with no project", func() {
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{},
})
So(err, ShouldBeRPCInvalidArgument)
})
Convey("with nonexistent project", func() {
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: "bogus"},
})
So(err, ShouldBeNil)
So(resp.Runs, ShouldBeEmpty)
So(resp.NextPageToken, ShouldBeEmpty)
})
Convey("with no runs", func() {
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
})
So(err, ShouldBeNil)
So(resp.Runs, ShouldBeEmpty)
So(resp.NextPageToken, ShouldBeEmpty)
})
// Add example data for tests below.
gHost := "r-review.example.com"
epoch := testclock.TestRecentTimeUTC.Truncate(time.Millisecond)
// putRun puts a Run and RunCLs in datastore and returns the Run.
putRun := func(proj string, delay time.Duration, cls ...*changelist.CL) *run.Run {
createdAt := epoch.Add(delay)
// As long as each RunID used in a given test is unique, RunID
// details don't matter. So practically, each Run must have a
// different create time or different set of CLs.
var clsDigest []byte
for _, cl := range cls {
clsDigest = append(clsDigest, []byte(cl.ExternalID)...)
}
runID := common.MakeRunID(proj, createdAt, 1, clsDigest)
clids := make(common.CLIDs, len(cls))
for i, cl := range cls {
clids[i] = common.CLID(cl.ID)
}
r := &run.Run{
ID: runID,
Status: run.Status_SUCCEEDED,
CLs: clids,
CreateTime: createdAt,
StartTime: createdAt.Add(time.Second),
UpdateTime: createdAt.Add(time.Minute),
EndTime: createdAt.Add(time.Hour),
Owner: "user:foo@example.org",
}
So(datastore.Put(ctx, r), ShouldBeNil)
for _, cl := range cls {
So(datastore.Put(ctx, &run.RunCL{
Run: datastore.MakeKey(ctx, common.RunKind, string(runID)),
ID: cl.ID,
IndexedID: cl.ID,
ExternalID: cl.ExternalID,
Detail: &changelist.Snapshot{Patchset: 1},
}), ShouldBeNil)
}
return r
}
Convey("with matching Runs, project-only predicate", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
r1 := putRun(projectName, 1*time.Millisecond, cl1)
r2 := putRun(projectName, 5*time.Millisecond, cl2)
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
})
So(err, ShouldBeNil)
// Most recent Run comes first.
So(respIDs(resp.Runs), ShouldResemble, runIDs(r2, r1))
So(resp.NextPageToken, ShouldBeEmpty)
})
Convey("paging, project-only predicate", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
r1 := putRun(projectName, 1*time.Millisecond, cl1)
r2 := putRun(projectName, 5*time.Millisecond, cl2)
// First request, first page.
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
PageSize: 1,
})
So(err, ShouldBeNil)
So(respIDs(resp.Runs), ShouldResemble, runIDs(r2))
So(resp.NextPageToken, ShouldNotBeEmpty)
// Second request, second page.
resp, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
PageSize: 1,
PageToken: resp.NextPageToken,
})
So(err, ShouldBeNil)
So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
So(resp.NextPageToken, ShouldNotBeEmpty)
// Third request, no more results.
resp, err = srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{Project: projectName},
PageSize: 1,
PageToken: resp.NextPageToken,
})
So(err, ShouldBeNil)
So(resp.Runs, ShouldBeEmpty)
So(resp.NextPageToken, ShouldBeEmpty)
})
Convey("with matching Run, single CL predicate", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
r1 := putRun(projectName, 1*time.Millisecond, cl1)
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
Project: projectName,
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{Host: gHost, Change: 1},
},
},
})
So(err, ShouldBeNil)
So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
})
Convey("with CL predicate that includes patchset", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
putRun(projectName, 1*time.Millisecond, cl1)
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
Project: projectName,
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{Host: gHost, Change: 1, Patchset: 3},
},
},
})
So(err, ShouldBeRPCInvalidArgument)
})
Convey("with CL predicate and no project given", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
putRun(projectName, 1*time.Millisecond, cl1)
_, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{Host: gHost, Change: 1},
},
},
})
So(err, ShouldBeRPCInvalidArgument)
})
Convey("with no matching Run, CL predicate", func() {
// No Runs put.
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
Project: projectName,
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{Host: gHost, Change: 1},
},
},
})
So(err, ShouldBeNil)
So(resp.Runs, ShouldBeEmpty)
})
Convey("query with multiple CLs returns Run that contains all CLs", func() {
cl1 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
cl2 := changelist.MustGobID(gHost, 3).MustCreateIfNotExists(ctx)
r1 := putRun(projectName, 1*time.Millisecond, cl1, cl2)
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
Project: projectName,
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{
Host: gHost,
Change: 2,
},
&apiv0pb.GerritChange{
Host: gHost,
Change: 3,
},
},
},
})
So(err, ShouldBeNil)
So(respIDs(resp.Runs), ShouldResemble, runIDs(r1))
})
Convey("query with multiple CLs returns nothing if no single CL contains all CLs", func() {
cl1 := changelist.MustGobID(gHost, 1).MustCreateIfNotExists(ctx)
cl2 := changelist.MustGobID(gHost, 2).MustCreateIfNotExists(ctx)
putRun(projectName, 1*time.Millisecond, cl1)
putRun(projectName, 5*time.Millisecond, cl2)
resp, err := srv.SearchRuns(ctx, &apiv0pb.SearchRunsRequest{
Predicate: &apiv0pb.RunPredicate{
Project: projectName,
GerritChanges: []*apiv0pb.GerritChange{
&apiv0pb.GerritChange{
Host: gHost,
Change: 1,
},
&apiv0pb.GerritChange{
Host: gHost,
Change: 2,
},
},
},
})
So(err, ShouldBeNil)
So(resp.Runs, ShouldBeEmpty)
})
})
}
func respIDs(runs []*apiv0pb.Run) common.RunIDs {
var ret common.RunIDs
for _, r := range runs {
id, err := common.FromPublicRunID(r.Id)
if err != nil {
panic(err)
}
ret = append(ret, id)
}
return ret
}
func runIDs(runs ...*run.Run) common.RunIDs {
var ret common.RunIDs
for _, r := range runs {
ret = append(ret, r.ID)
}
return ret
}