blob: 1d7c412d0f34db7043875d52ea05694bc3dd5e99 [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 migration
import (
"context"
"testing"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
cvbqpb "go.chromium.org/luci/cv/api/bigquery/v1"
migrationpb "go.chromium.org/luci/cv/api/migration"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/cvtesting"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/run"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestClsOf(t *testing.T) {
t.Parallel()
Convey("clsOf works", t, func() {
a := &cvbqpb.Attempt{}
So(clsOf(a), ShouldEqual, "NO CLS")
a.GerritChanges = []*cvbqpb.GerritChange{
{Host: "abc", Change: 1, Patchset: 2},
}
So(clsOf(a), ShouldEqual, "1 CLs: [abc 1/2]")
a.GerritChanges = []*cvbqpb.GerritChange{
{Host: "abc", Change: 1, Patchset: 2},
{Host: "abc", Change: 2, Patchset: 3},
}
So(clsOf(a), ShouldEqual, "2 CLs: [abc 1/2 2/3]")
a.GerritChanges = []*cvbqpb.GerritChange{
{Host: "abc", Change: 1, Patchset: 2},
{Host: "xyz", Change: 2, Patchset: 3},
{Host: "xyz", Change: 3, Patchset: 4},
{Host: "abc", Change: 4, Patchset: 5},
{Host: "abc", Change: 5, Patchset: 6},
}
So(clsOf(a), ShouldEqual, "5 CLs: [abc 1/2] [xyz 2/3 3/4] [abc 4/5 5/6]")
})
}
func TestPostGerritMessage(t *testing.T) {
t.Parallel()
Convey("PostGerritMessage works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
m := MigrationServer{
GFactory: ct.GFactory(),
}
req := &migrationpb.PostGerritMessageRequest{
AttemptKey: "deadbeef",
RunId: "infra/123-1-deadbeef",
Project: "infra",
Change: 1,
Host: "g-review.example.com",
Revision: "cqd-seen-revision",
Comment: "some verification failed",
SendEmail: true,
}
Convey("no permission to call CV", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: "user:hack-for-fun-and-profit@example.com",
})
_, err := m.PostGerritMessage(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
Convey("with permission to call CV", func() {
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.Identity("project:infra"),
PeerIdentityOverride: "user:cqdaemon@example.com",
})
// Without Run.
_, err := m.PostGerritMessage(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.Unavailable)
// Put a Run.
r := &run.Run{
ID: common.RunID(req.GetRunId()),
Mode: run.DryRun,
Status: run.Status_RUNNING,
CreateTime: ct.Clock.Now().Add(-time.Hour).UTC(),
}
So(datastore.Put(ctx, r), ShouldBeNil)
// Without CL in CV.
_, err = m.PostGerritMessage(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.Unavailable)
// Put CL in CV (CL in Gerrit is faked separately below).
ci := gf.CI(int(req.GetChange()))
cl := changelist.MustGobID(req.GetHost(), req.GetChange()).MustCreateIfNotExists(ctx)
cl.Snapshot = &changelist.Snapshot{
ExternalUpdateTime: ci.GetUpdated(),
LuciProject: req.GetProject(),
MinEquivalentPatchset: 1,
Patchset: 1,
Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{Info: ci}},
}
So(datastore.Put(ctx, cl), ShouldBeNil)
Convey("propagates Gerrit errors", func() {
Convey("404", func() {
ct.GFake.AddFrom(gf.WithCIs(req.GetHost(), gf.ACLRestricted("other-project"), ci))
_, err = m.PostGerritMessage(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
})
Convey("403", func() {
ct.GFake.AddFrom(gf.WithCIs(req.GetHost(), gf.ACLReadOnly(req.GetProject()), ci))
_, err = m.PostGerritMessage(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied)
})
})
Convey("with Gerrit permission, posts to Gerrit", func() {
ct.GFake.AddFrom(gf.WithCIs(req.GetHost(), gf.ACLPublic(), ci))
mod := ci.GetUpdated().AsTime()
_, err = m.PostGerritMessage(ctx, req)
So(err, ShouldBeNil)
ci2 := ct.GFake.GetChange(req.GetHost(), int(req.GetChange())).Info
So(ci2, gf.ShouldLastMessageContain, req.GetComment())
mod2 := ci2.GetUpdated().AsTime()
So(mod2, ShouldHappenAfter, mod)
Convey("but avoids duplication", func() {
cl.Snapshot.GetGerrit().Info = ci2
cl.Snapshot.ExternalUpdateTime = ci2.GetUpdated()
So(datastore.Put(ctx, cl), ShouldBeNil)
_, err = m.PostGerritMessage(ctx, req)
So(err, ShouldBeNil)
ci3 := ct.GFake.GetChange(req.GetHost(), int(req.GetChange())).Info
mod3 := ci3.GetUpdated().AsTime()
So(mod3, ShouldResemble, mod2)
})
Convey("posts a duplicate if prior message was before this Run started", func() {
r.CreateTime = mod2.Add(time.Minute)
So(datastore.Put(ctx, r), ShouldBeNil)
ct.Clock.Set(r.CreateTime.Add(time.Hour))
_, err = m.PostGerritMessage(ctx, req)
So(err, ShouldBeNil)
ci3 := ct.GFake.GetChange(req.GetHost(), int(req.GetChange())).Info
mod3 := ci3.GetUpdated().AsTime()
So(mod3, ShouldHappenAfter, r.CreateTime)
})
})
})
})
}
func TestReportVerifiedRun(t *testing.T) {
t.Parallel()
Convey("ReportVerifiedRun works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
rnMock := runNotifierMock{}
m := MigrationServer{RunNotifier: &rnMock}
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.Identity("project:infra"),
PeerIdentityOverride: "user:cqdaemon@example.com",
})
const runID = common.RunID("infra/111-1-deadbeef")
req := &migrationpb.ReportVerifiedRunRequest{
Run: &migrationpb.ReportedRun{
Id: string(runID),
Attempt: &cvbqpb.Attempt{
Key: runID.AttemptKey(),
Status: cvbqpb.AttemptStatus_SUCCESS,
// In practice, the other fields are also set by CQDaemon, but not
// relevant in this test.
},
},
FinalMessage: "meh",
Action: migrationpb.ReportVerifiedRunRequest_ACTION_SUBMIT,
}
loadVerifiedCQDRun := func() *VerifiedCQDRun {
v := &VerifiedCQDRun{ID: runID}
switch err := datastore.Get(ctx, v); {
case err == datastore.ErrNoSuchEntity:
return nil
case err != nil:
panic(err)
default:
return v
}
}
Convey("without a Run in Datastore", func() {
_, err := m.ReportVerifiedRun(ctx, req)
So(grpcutil.Code(err), ShouldEqual, codes.NotFound)
So(loadVerifiedCQDRun(), ShouldBeNil)
})
Convey("with a Run, always saves and notifies Run Manager", func() {
So(datastore.Put(ctx, &run.Run{ID: runID}), ShouldBeNil)
_, err := m.ReportVerifiedRun(ctx, proto.Clone(req).(*migrationpb.ReportVerifiedRunRequest))
So(err, ShouldBeNil)
first := loadVerifiedCQDRun()
So(first.Payload.Run.Attempt.Status, ShouldEqual, cvbqpb.AttemptStatus_SUCCESS)
So(rnMock.verificationCompleted, ShouldContain, runID)
Convey("does not overwrite existing data", func() {
rnMock.verificationCompleted = nil
req.Run.Attempt.Status = cvbqpb.AttemptStatus_INFRA_FAILURE
_, err := m.ReportVerifiedRun(ctx, proto.Clone(req).(*migrationpb.ReportVerifiedRunRequest))
So(err, ShouldBeNil)
second := loadVerifiedCQDRun()
So(second.UpdateTime, ShouldResemble, first.UpdateTime)
So(second.Payload, ShouldResembleProto, first.Payload)
// Run Manager doesn't need to be notified again.
So(rnMock.verificationCompleted, ShouldBeEmpty)
})
})
})
}
func TestReportTryjobs(t *testing.T) {
t.Parallel()
Convey("ReportTryjobs works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
rnMock := runNotifierMock{}
m := MigrationServer{RunNotifier: &rnMock}
ctx = auth.WithState(ctx, &authtest.FakeState{
Identity: identity.Identity("project:infra"),
PeerIdentityOverride: "user:cqdaemon@example.com",
})
const runID = common.RunID("infra/111-1-deadbeef")
req := &migrationpb.ReportTryjobsRequest{
RunId: string(runID),
Tryjobs: []*migrationpb.Tryjob{
{
Status: migrationpb.TryjobStatus_PENDING,
Build: &cvbqpb.Build{Id: 123, Origin: cvbqpb.Build_NOT_REUSED},
},
{
Status: migrationpb.TryjobStatus_RUNNING,
Build: &cvbqpb.Build{Id: 124, Origin: cvbqpb.Build_REUSED},
},
},
}
_, err := m.ReportTryjobs(ctx, req)
So(err, ShouldBeNil)
ct.Clock.Add(time.Minute)
req.Tryjobs[0].Status = migrationpb.TryjobStatus_RUNNING
_, err = m.ReportTryjobs(ctx, req)
So(err, ShouldBeNil)
ct.Clock.Add(time.Minute)
req.Tryjobs[1].Status = migrationpb.TryjobStatus_SUCCEEDED
_, err = m.ReportTryjobs(ctx, req)
So(err, ShouldBeNil)
So(rnMock.tryjobsUpdated, ShouldResemble, common.RunIDs{runID, runID, runID})
all, err := ListReportedTryjobs(ctx, runID, ct.Clock.Now().Add(-time.Hour), 0 /*unlimited*/)
So(err, ShouldBeNil)
So(all, ShouldHaveLength, 3)
})
}
type runNotifierMock struct {
verificationCompleted common.RunIDs
tryjobsUpdated common.RunIDs
finished common.RunIDs
}
func (r *runNotifierMock) NotifyCQDVerificationCompleted(ctx context.Context, runID common.RunID) error {
r.verificationCompleted = append(r.verificationCompleted, runID)
return nil
}
func (r *runNotifierMock) NotifyCQDTryjobsUpdated(ctx context.Context, runID common.RunID) error {
r.tryjobsUpdated = append(r.tryjobsUpdated, runID)
return nil
}
func (r *runNotifierMock) NotifyCQDFinished(ctx context.Context, runID common.RunID) error {
r.finished = append(r.finished, runID)
return nil
}