blob: a00d4b259dca30328a73869b60f5e189801d97e3 [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 e2e
import (
"fmt"
"sort"
"testing"
"time"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/gae/service/datastore"
"google.golang.org/protobuf/types/known/timestamppb"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/configs/prjcfg/prjcfgtest"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/tryjob"
. "github.com/smartystreets/goconvey/convey"
)
func TestNewPatchsetUploadRun(t *testing.T) {
t.Parallel()
Convey("Non-combinable", t, func() {
ct := Test{}
ctx, cancel := ct.SetUp(t)
defer cancel()
const lProject = "infra"
const gHost = "g-review"
const gRepo = "re/po"
const gRef = "refs/heads/main"
const gChangeFirst = 1001
cfg := MakeCfgSingular("cg0", gHost, gRepo, gRef, &cfgpb.Verifiers_Tryjob_Builder{
Host: buildbucketHost,
Name: fmt.Sprintf("%s/test.bucket/static-analyzer", lProject),
ModeAllowlist: []string{string(run.NewPatchsetRun)},
})
ct.BuildbucketFake.EnsureBuilders(cfg)
prjcfgtest.Create(ctx, lProject, cfg)
Convey("A single patchset is uploaded", func() {
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.AddCommitter("uploader-99")
ct.AddNewPatchsetRunner("uploader-99")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeFirst,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("uploader-99"), gf.PSWithUploader(1, "uploader-99", updated),
)))
var rs []*run.Run
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
return len(rs) > 0
})
So(rs, ShouldHaveLength, 1)
So(rs[0].Mode, ShouldEqual, run.NewPatchsetRun)
ct.Clock.Add(5 * time.Minute)
Convey("And then commit-queued", func() {
ct.GFake.MutateChange(gHost, gChangeFirst, func(c *gf.Change) {
gf.CQ(2, ct.Clock.Now(), "uploader-99")(c.Info)
gf.Updated(ct.Clock.Now())(c.Info)
})
So(ct.GFake.GetChange(gHost, gChangeFirst).Info.Labels["Commit-Queue"].All[0].Value, ShouldEqual, 2)
// The new run should be created.
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
for _, r := range rs {
return r.Mode == run.FullRun
}
return false
})
// We should now have one run in each mode.
So(rs, ShouldHaveLength, 2)
modes := make(map[run.Mode]bool)
for _, r := range rs {
modes[r.Mode] = true
}
So(modes[run.NewPatchsetRun], ShouldBeTrue)
So(modes[run.FullRun], ShouldBeTrue)
})
Convey("And then a new patchset is uploaded", func() {
originalRun := rs[0]
originalID := rs[0].ID
now := ct.Clock.Now()
ct.GFake.MutateChange(gHost, gChangeFirst, func(c *gf.Change) {
gf.PSWithUploader(2, "uploader-100", now)(c.Info)
gf.Updated(ct.Clock.Now())(c.Info)
})
// A new run should be created.
var newRun *run.Run
ct.RunUntil(ctx, func() bool {
runs := ct.LoadRunsOf(ctx, lProject)
for _, r := range runs {
if r.ID == originalID {
originalRun = r
} else {
newRun = r
}
}
return newRun != nil
})
So(newRun, ShouldNotBeNil)
So(newRun.Mode, ShouldEqual, run.NewPatchsetRun)
So(newRun.Status, ShouldEqual, run.Status_PENDING)
So(originalRun.Status, ShouldEqual, run.Status_CANCELLED)
})
})
Convey("A single patchset is uploaded, and commit-queued at the same time", func() {
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.AddCommitter("uploader-99")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeFirst,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("uploader-99"), gf.PSWithUploader(1, "uploader-99", updated),
gf.CQ(2),
)))
// Wait for both Runs to be created.
var rs []*run.Run
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
return len(rs) > 1
})
So(rs, ShouldHaveLength, 2)
modes := make(map[run.Mode]bool, 2)
for _, r := range rs {
modes[r.Mode] = true
}
So(modes[run.NewPatchsetRun], ShouldBeTrue)
So(modes[run.FullRun], ShouldBeTrue)
})
Convey("A patchset is uploaded after a previous patchset upload run is complete", func() {
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.AddCommitter("uploader-99")
ct.AddNewPatchsetRunner("uploader-99")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeFirst,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("uploader-99"), gf.PSWithUploader(1, "uploader-99", updated),
gf.CQ(0, time.Time{}, "uploader-99"),
)))
// Wait until the first Run starts.
var newRun, originalRun *run.Run
ct.RunUntil(ctx, func() bool {
rs := ct.LoadRunsOf(ctx, lProject)
if len(rs) > 0 {
So(rs, ShouldHaveLength, 1)
originalRun = rs[0]
return true
}
return false
})
So(originalRun.Mode, ShouldEqual, run.NewPatchsetRun)
// Make the first Run succeed.
ct.LogPhase(ctx, "Tryjob for the first Run has passed")
var buildID int64
ct.RunUntil(ctx, func() bool {
// Check whether the build has been successfully triggered and get the
// build ID.
r := ct.LoadRun(ctx, originalRun.ID)
if executions := r.Tryjobs.GetState().GetExecutions(); len(executions) > 0 {
if eid := tryjob.LatestAttempt(executions[0]).GetExternalId(); eid != "" {
_, buildID = tryjob.ExternalID(eid).MustParseBuildbucketID()
return true
}
}
return false
})
ct.Clock.Add(time.Minute)
ct.BuildbucketFake.MutateBuild(ctx, buildbucketHost, buildID, func(b *buildbucketpb.Build) {
b.Status = buildbucketpb.Status_SUCCESS
b.StartTime = timestamppb.New(ct.Clock.Now())
b.EndTime = timestamppb.New(ct.Clock.Now())
})
ct.RunUntil(ctx, func() bool {
originalRun = ct.LoadRun(ctx, originalRun.ID)
return run.IsEnded(originalRun.Status)
})
So(originalRun.Status, ShouldEqual, run.Status_SUCCEEDED)
// Upload a new patch, wait for it to create a new run.
now := ct.Clock.Now()
ct.GFake.MutateChange(gHost, gChangeFirst, func(c *gf.Change) {
gf.PS(2)(c.Info)
gf.PSWithUploader(2, "uploader-100", now)(c.Info)
c.Info.Updated = timestamppb.New(now)
})
ct.RunUntil(ctx, func() bool {
for _, r := range ct.LoadRunsOf(ctx, lProject) {
if r.ID != originalRun.ID {
newRun = r
return true
}
}
return false
})
So(newRun.Mode, ShouldEqual, run.NewPatchsetRun)
So(ct.LoadCL(ctx, newRun.CLs[0]).Snapshot.Patchset, ShouldEqual, 2)
})
Convey("An untriggered dependent CL is uploaded, this should create an NP Run and not a CQ Run", func() {
// CL A depends on CL B, CL A is CQ+2 but CL B is not.
// This should create two NPR Runs and no CQ Run.
gChangeA, gChangeB := 1002, 1003
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.AddDryRunner("user-1")
ct.AddNewPatchsetRunner("user-1")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeA,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("user-1"), gf.PSWithUploader(1, "user-1", updated),
gf.CQ(1, updated, "user-1"),
), gf.CI(
gChangeB,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("user-1"), gf.PSWithUploader(1, "user-1", updated),
)))
ct.GFake.SetDependsOn(gHost, fmt.Sprintf("%d_1", gChangeA), fmt.Sprintf("%d_1", gChangeB))
// Wait for all three runs to be created.
var rs []*run.Run
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
return len(rs) >= 3
})
So(len(rs), ShouldEqual, 3)
// Sort them by mode, then external ID.
sort.Slice(rs, func(i, j int) bool {
if rs[i].Mode != rs[j].Mode {
return rs[i].Mode < rs[j].Mode
}
clI := &changelist.CL{ID: rs[i].CLs[0]}
clJ := &changelist.CL{ID: rs[j].CLs[0]}
So(datastore.Get(ctx, clI, clJ), ShouldBeNil)
return clI.ExternalID < clJ.ExternalID
})
cla, err := changelist.MustGobID(gHost, int64(gChangeA)).Load(ctx)
So(err, ShouldBeNil)
clb, err := changelist.MustGobID(gHost, int64(gChangeB)).Load(ctx)
So(err, ShouldBeNil)
So(rs[0].Mode, ShouldEqual, run.DryRun)
So(rs[0].CLs, ShouldHaveLength, 1)
So(rs[0].CLs[0], ShouldEqual, cla.ID)
So(rs[1].Mode, ShouldEqual, run.NewPatchsetRun)
So(rs[1].CLs, ShouldHaveLength, 1)
So(rs[1].CLs[0], ShouldEqual, cla.ID)
So(rs[2].Mode, ShouldEqual, run.NewPatchsetRun)
So(rs[2].CLs, ShouldHaveLength, 1)
So(rs[2].CLs[0], ShouldEqual, clb.ID)
})
Convey("If user is not authorized then the new patchset run is immediately cancelled", func() {
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.GFake.AddLinkedAccountMapping([]*gerritpb.EmailInfo{
&gerritpb.EmailInfo{Email: "uploader-99@example.com"},
})
ct.AddCommitter("uploader-99")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeFirst,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("uploader-99"), gf.PSWithUploader(1, "uploader-99", updated),
gf.CQ(0, time.Time{}, "uploader-99"),
)))
// The run should be created and immediately failed.
var rs []*run.Run
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
for _, r := range rs {
return r.Status == run.Status_FAILED
}
return false
})
So(ct.GFake.GetChange(gHost, gChangeFirst).Info.Messages, ShouldHaveLength, 0)
})
})
Convey("Combinable", t, func() {
ct := Test{}
ctx, cancel := ct.SetUp(t)
defer cancel()
const lProject = "infra"
const gHost = "g-review"
const gRepo = "re/po"
const gRef = "refs/heads/main"
cfg := MakeCfgCombinable("cg0", gHost, gRepo, gRef, &cfgpb.Verifiers_Tryjob_Builder{
Host: buildbucketHost,
Name: fmt.Sprintf("%s/test.bucket/static-analyzer", lProject),
ModeAllowlist: []string{string(run.NewPatchsetRun)},
})
ct.BuildbucketFake.EnsureBuilders(cfg)
prjcfgtest.Create(ctx, lProject, cfg)
// A depends on B, both are ready. We should get three runs. Two NPR and one CQ
gChangeA, gChangeB := 1002, 1003
So(ct.PMNotifier.UpdateConfig(ctx, lProject), ShouldBeNil)
updated := ct.Clock.Now().Add(time.Minute)
ct.AddDryRunner("user-1")
ct.AddNewPatchsetRunner("user-1")
ct.GFake.AddFrom(gf.WithCIs(gHost, gf.ACLRestricted(lProject), gf.CI(
gChangeA,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("user-1"), gf.PSWithUploader(1, "user-1", updated),
gf.CQ(2, updated, "user-1"),
), gf.CI(
gChangeB,
gf.Updated(updated),
gf.Project(gRepo), gf.Ref(gRef),
gf.Owner("user-1"), gf.PSWithUploader(1, "user-1", updated),
gf.CQ(2, updated, "user-1"),
)))
ct.GFake.SetDependsOn(gHost, fmt.Sprintf("%d_1", gChangeA), fmt.Sprintf("%d_1", gChangeB))
// Wait for all three runs to be created.
var rs []*run.Run
ct.RunUntil(ctx, func() bool {
rs = ct.LoadRunsOf(ctx, lProject)
return len(rs) >= 3
})
So(len(rs), ShouldEqual, 3)
// Sort by mode then by external ID of the first CL in the Run.
sort.Slice(rs, func(i, j int) bool {
if rs[i].Mode != rs[j].Mode {
return rs[i].Mode < rs[j].Mode
}
clI := &changelist.CL{ID: rs[i].CLs[0]}
clJ := &changelist.CL{ID: rs[j].CLs[0]}
So(datastore.Get(ctx, clI, clJ), ShouldBeNil)
return clI.ExternalID < clJ.ExternalID
})
cla, err := changelist.MustGobID(gHost, int64(gChangeA)).Load(ctx)
So(err, ShouldBeNil)
clb, err := changelist.MustGobID(gHost, int64(gChangeB)).Load(ctx)
So(err, ShouldBeNil)
// The full run includes both CLs.
So(rs[0].Mode, ShouldEqual, run.FullRun)
So(rs[0].CLs, ShouldHaveLength, 2)
actual := rs[0].CLs
actual.Dedupe() // Sort.
expected := common.CLIDs{cla.ID, clb.ID}
expected.Dedupe() // Sort.
So(actual, ShouldResemble, expected)
// The new patchset runs each include one CL.
So(rs[1].Mode, ShouldEqual, run.NewPatchsetRun)
So(rs[1].CLs, ShouldHaveLength, 1)
So(rs[1].CLs[0], ShouldEqual, cla.ID)
So(rs[2].Mode, ShouldEqual, run.NewPatchsetRun)
So(rs[2].CLs, ShouldHaveLength, 1)
So(rs[2].CLs[0], ShouldEqual, clb.ID)
})
}