blob: 90ddde6452198bccda04f24a4981c1213cb9d3f5 [file] [log] [blame]
// Copyright 2021 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 clpurger
import (
"context"
"testing"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/tq/tqtesting"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
"go.chromium.org/luci/cv/internal/cvtesting"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/gerrit/gobmap/gobmaptest"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
gerritupdater "go.chromium.org/luci/cv/internal/gerrit/updater"
"go.chromium.org/luci/cv/internal/prjmanager"
"go.chromium.org/luci/cv/internal/prjmanager/pmtest"
"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
"go.chromium.org/luci/cv/internal/tryjob"
"go.chromium.org/luci/cv/internal/tryjob/tjcancel"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestPurgeCL(t *testing.T) {
t.Parallel()
Convey("PurgeCL works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
ctx, pmDispatcher := pmtest.MockDispatch(ctx)
pmNotifier := prjmanager.NewNotifier(ct.TQDispatcher)
tjNotifier := tryjob.NewNotifier(ct.TQDispatcher)
_ = tjcancel.NewCancellator(tjNotifier)
clMutator := changelist.NewMutator(ct.TQDispatcher, pmNotifier, nil, tjNotifier)
fakeCLUpdater := clUpdaterMock{}
purger := New(pmNotifier, ct.GFactory(), &fakeCLUpdater)
const lProject = "lprj"
const gHost = "x-review"
const gRepo = "repo"
const change = 43
cfg := makeConfig(gHost, gRepo)
prjcfgtest.Create(ctx, lProject, cfg)
cfgMeta := prjcfgtest.MustExist(ctx, lProject)
gobmaptest.Update(ctx, lProject)
// Fake 1 CL in gerrit & import it to Datastore.
ci := gf.CI(
change, gf.PS(2), gf.Project(gRepo), gf.Ref("refs/heads/main"),
gf.CQ(+2, ct.Clock.Now().Add(-2*time.Minute), gf.U("user-1")),
gf.Updated(ct.Clock.Now().Add(-1*time.Minute)),
)
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), ci))
// The real CL Updater for realistic CL Snapshot in datastore.
clUpdater := changelist.NewUpdater(ct.TQDispatcher, clMutator)
gerritupdater.RegisterUpdater(clUpdater, ct.GFactory())
refreshCL := func() {
So(clUpdater.TestingForceUpdate(ctx, &changelist.UpdateCLTask{
LuciProject: lProject,
ExternalId: string(changelist.MustGobID(gHost, change)),
}), ShouldBeNil)
}
refreshCL()
loadCL := func() *changelist.CL {
cl, err := changelist.MustGobID(gHost, change).Load(ctx)
So(err, ShouldBeNil)
So(cl, ShouldNotBeNil)
return cl
}
clBefore := loadCL()
assertPMNotified := func(op string) {
pmtest.AssertInEventbox(ctx, lProject, &prjpb.Event{Event: &prjpb.Event_PurgeCompleted{
PurgeCompleted: &prjpb.PurgeCompleted{
OperationId: op,
},
}})
}
// Basic task.
task := &prjpb.PurgeCLTask{
LuciProject: lProject,
PurgingCl: &prjpb.PurgingCL{
OperationId: "op",
Clid: int64(clBefore.ID),
Deadline: timestamppb.New(ct.Clock.Now().Add(10 * time.Minute)),
},
Trigger: trigger.Find(ci, cfg.GetConfigGroups()[0]).GetCqVoteTrigger(),
Reasons: []*changelist.CLError{
{Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true}},
},
ConfigGroups: []string{string(cfgMeta.ConfigGroupIDs[0])},
}
So(task.Trigger, ShouldNotBeNil)
schedule := func() error {
return datastore.RunInTransaction(ctx, func(tCtx context.Context) error {
return purger.Schedule(tCtx, task)
}, nil)
}
ct.Clock.Add(time.Minute)
Convey("Happy path: cancel trigger, schedule CL refresh, and notify PM", func() {
So(schedule(), ShouldBeNil)
ct.TQ.Run(ctx, tqtesting.StopAfterTask(prjpb.PurgeProjectCLTaskClass))
ciAfter := ct.GFake.GetChange(gHost, change).Info
So(trigger.Find(ciAfter, cfg.GetConfigGroups()[0]), ShouldBeNil)
So(ciAfter, gf.ShouldLastMessageContain, "owner doesn't have a preferred email")
So(fakeCLUpdater.scheduledTasks, ShouldHaveLength, 1)
assertPMNotified("op")
Convey("Idempotent: if TQ task is retried, just notify PM", func() {
verifyIdempotency := func() {
// Use different Operation ID s.t. we can easily assert PM was notified
// the 2nd time.
task.PurgingCl.OperationId = "op-2"
So(schedule(), ShouldBeNil)
ct.TQ.Run(ctx, tqtesting.StopAfterTask(prjpb.PurgeProjectCLTaskClass))
// CL in Gerrit shouldn't be changed.
ciAfter2 := ct.GFake.GetChange(gHost, change).Info
So(ciAfter2, ShouldResembleProto, ciAfter)
// But PM must be notified.
assertPMNotified("op-2")
So(pmDispatcher.LatestETAof(lProject), ShouldHappenBefore, ct.Clock.Now().Add(2*time.Second))
}
// Idempotency must not rely on CL being updated between retries.
Convey("CL updated between retries", func() {
verifyIdempotency()
})
Convey("CL not updated between retries", func() {
refreshCL()
verifyIdempotency()
})
})
})
Convey("Even if no purging is done, PM is always notified", func() {
Convey("Task arrives after the deadline", func() {
task.PurgingCl.Deadline = timestamppb.New(ct.Clock.Now().Add(-time.Minute))
So(schedule(), ShouldBeNil)
ct.TQ.Run(ctx, tqtesting.StopAfterTask(prjpb.PurgeProjectCLTaskClass))
So(loadCL().EVersion, ShouldEqual, clBefore.EVersion) // no changes.
assertPMNotified("op")
So(pmDispatcher.LatestETAof(lProject), ShouldHappenBefore, ct.Clock.Now().Add(2*time.Second))
})
Convey("Trigger is no longer matching latest CL Snapshot", func() {
// Simulate old trigger for CQ+1, while snapshot contains CQ+2.
gf.CQ(+1, ct.Clock.Now().Add(-time.Hour), gf.U("user-1"))(ci)
task.Trigger = trigger.Find(ci, cfg.GetConfigGroups()[0]).GetCqVoteTrigger()
So(schedule(), ShouldBeNil)
ct.TQ.Run(ctx, tqtesting.StopAfterTask(prjpb.PurgeProjectCLTaskClass))
So(loadCL().EVersion, ShouldEqual, clBefore.EVersion) // no changes.
assertPMNotified("op")
// The PM task should be ASAP.
So(pmDispatcher.LatestETAof(lProject), ShouldHappenBefore, ct.Clock.Now().Add(2*time.Second))
})
})
})
}
func makeConfig(gHost string, gRepo string) *cfgpb.Config {
return &cfgpb.Config{
ConfigGroups: []*cfgpb.ConfigGroup{
{
Name: "main",
Gerrit: []*cfgpb.ConfigGroup_Gerrit{
{
Url: "https://" + gHost + "/",
Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
{
Name: gRepo,
RefRegexp: []string{"refs/heads/main"},
},
},
},
},
},
},
}
}
type clUpdaterMock struct {
scheduledTasks []*changelist.UpdateCLTask
}
func (c *clUpdaterMock) Schedule(_ context.Context, task *changelist.UpdateCLTask) error {
c.scheduledTasks = append(c.scheduledTasks, task)
return nil
}