blob: ff5133f0b58c84d1ad0ad634cc67d067d8cb174c [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 triager
import (
"fmt"
"sort"
"testing"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/gae/service/datastore"
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"
"go.chromium.org/luci/cv/internal/cvtesting"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
"go.chromium.org/luci/cv/internal/prjmanager/itriager"
"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
"go.chromium.org/luci/cv/internal/run"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestTriage(t *testing.T) {
t.Parallel()
Convey("Triage works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
// Truncate start time point s.t. easy to see diff in test failures.
ct.RoundTestClock(10000 * time.Second)
const gHost = "g-review.example.com"
const lProject = "v8"
const stabilizationDelay = 5 * time.Minute
const singIdx, combIdx, anotherIdx, nprIdx, nprCombIdx = 0, 1, 2, 3, 4
cfg := &cfgpb.Config{
ConfigGroups: []*cfgpb.ConfigGroup{
{Name: "singular"},
{Name: "combinable", CombineCls: &cfgpb.CombineCLs{
StabilizationDelay: durationpb.New(stabilizationDelay),
}},
{Name: "another"},
{Name: "newPatchsetRun",
Verifiers: &cfgpb.Verifiers{
Tryjob: &cfgpb.Verifiers_Tryjob{
Builders: []*cfgpb.Verifiers_Tryjob_Builder{
{Name: "nprBuilder", ModeAllowlist: []string{string(run.NewPatchsetRun)}},
},
},
},
},
{Name: "newPatchsetRunCombi",
Verifiers: &cfgpb.Verifiers{
Tryjob: &cfgpb.Verifiers_Tryjob{
Builders: []*cfgpb.Verifiers_Tryjob_Builder{
{Name: "nprBuilder", ModeAllowlist: []string{string(run.NewPatchsetRun)}},
},
},
},
CombineCls: &cfgpb.CombineCLs{StabilizationDelay: durationpb.New(stabilizationDelay)},
},
},
}
prjcfgtest.Create(ctx, lProject, cfg)
pm := &simplePMState{pb: &prjpb.PState{}}
var err error
pm.cgs, err = prjcfgtest.MustExist(ctx, lProject).GetConfigGroups(ctx)
So(err, ShouldBeNil)
dryRun := func(t time.Time) *run.Triggers {
return &run.Triggers{CqVoteTrigger: &run.Trigger{Mode: string(run.DryRun), Time: timestamppb.New(t)}}
}
triage := func(c *prjpb.Component) (itriager.Result, error) {
backup := prjpb.PState{}
proto.Merge(&backup, pm.pb)
res, err := Triage(ctx, c, pm)
// Regardless of result, PM's state must be not be modified.
So(pm.pb, ShouldResembleProto, &backup)
return res, err
}
mustTriage := func(c *prjpb.Component) itriager.Result {
res, err := triage(c)
So(err, ShouldBeNil)
return res
}
failTriage := func(c *prjpb.Component) error {
_, err := triage(c)
So(err, ShouldNotBeNil)
return err
}
markTriaged := func(c *prjpb.Component) *prjpb.Component {
c = c.CloneShallow()
c.TriageRequired = false
return c
}
putPCL := func(clid int, grpIndex int32, mode run.Mode, triggerTime time.Time, depsCLIDs ...int) (*changelist.CL, *prjpb.PCL) {
mods := []gf.CIModifier{gf.PS(1), gf.Updated(triggerTime)}
u := gf.U("user-1")
switch mode {
case run.FullRun:
mods = append(mods, gf.CQ(+2, triggerTime, u))
case run.DryRun:
mods = append(mods, gf.CQ(+1, triggerTime, u))
case run.NewPatchsetRun:
default:
panic(fmt.Errorf("unsupported %s", mode))
}
ci := gf.CI(clid, mods...)
trs := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cfg.GetConfigGroups()[grpIndex]})
switch mode {
case run.NewPatchsetRun:
So(grpIndex, ShouldBeGreaterThanOrEqualTo, nprIdx)
So(trs.GetNewPatchsetRunTrigger(), ShouldNotBeNil)
default:
So(trs.GetCqVoteTrigger(), ShouldNotBeNil)
So(trs.GetCqVoteTrigger().GetMode(), ShouldResemble, string(mode))
}
cl := &changelist.CL{
ID: common.CLID(clid),
EVersion: 1,
Snapshot: &changelist.Snapshot{Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{
Host: gHost,
Info: ci,
}}},
}
for _, d := range depsCLIDs {
cl.Snapshot.Deps = append(cl.Snapshot.Deps, &changelist.Dep{
Clid: int64(d),
Kind: changelist.DepKind_SOFT,
})
}
So(datastore.Put(ctx, cl), ShouldBeNil)
pclTriggers := proto.Clone(trs).(*run.Triggers)
if pclTriggers.GetNewPatchsetRunTrigger() != nil {
pclTriggers.NewPatchsetRunTrigger.Email = ""
pclTriggers.NewPatchsetRunTrigger.GerritAccountId = 0
}
if pclTriggers.GetCqVoteTrigger() != nil {
pclTriggers.CqVoteTrigger.Email = ""
pclTriggers.CqVoteTrigger.GerritAccountId = 0
}
return cl, &prjpb.PCL{
Clid: int64(clid),
Eversion: 1,
Status: prjpb.PCL_OK,
ConfigGroupIndexes: []int32{grpIndex},
Triggers: pclTriggers,
Deps: cl.Snapshot.GetDeps(),
}
}
Convey("Noops", func() {
pcl := &prjpb.PCL{Clid: 33, ConfigGroupIndexes: []int32{singIdx}, Triggers: dryRun(ct.Clock.Now())}
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{
Clids: []int64{33},
// Component already has a Run, so no action required.
Pruns: []*prjpb.PRun{{Id: "id", Clids: []int64{33}, Mode: string(run.DryRun)}},
TriageRequired: true,
}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.RunsToCreate, ShouldBeEmpty)
So(res.CLsToPurge, ShouldBeEmpty)
})
Convey("Purges CLs", func() {
pm.pb.Pcls = []*prjpb.PCL{{
Clid: 33,
ConfigGroupIndexes: nil, // modified below.
Triggers: dryRun(ct.Clock.Now()),
PurgeReasons: []*prjpb.PurgeReason{{
ClError: &changelist.CLError{ // => must purge.
Kind: &changelist.CLError_OwnerLacksEmail{OwnerLacksEmail: true},
},
ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: dryRun(ct.Clock.Now())},
}},
}}
oldC := &prjpb.Component{Clids: []int64{33}}
Convey("singular group -- no delay", func() {
pm.pb.Pcls[0].ConfigGroupIndexes = []int32{singIdx}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("singular group, does not affect NPR", func() {
_, pcl := putPCL(33, nprIdx, run.DryRun, ct.Clock.Now())
pm.pb.Pcls[0] = pcl
pcl.PurgeReasons = []*prjpb.PurgeReason{{
ClError: &changelist.CLError{
Kind: &changelist.CLError_SelfCqDepend{},
},
ApplyTo: &prjpb.PurgeReason_Triggers{Triggers: dryRun(ct.Clock.Now())},
}}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldHaveLength, 1)
So(res.RunsToCreate[0].Mode, ShouldEqual, run.NewPatchsetRun)
})
Convey("NPR trigger only", func() {
_, pcl := putPCL(33, nprIdx, run.NewPatchsetRun, ct.Clock.Now())
pm.pb.Pcls[0] = pcl
pcl.PurgeReasons = []*prjpb.PurgeReason{{
ClError: &changelist.CLError{
Kind: &changelist.CLError_UnsupportedMode{},
},
ApplyTo: &prjpb.PurgeReason_Triggers{
Triggers: &run.Triggers{
NewPatchsetRunTrigger: &run.Trigger{
Mode: string(run.NewPatchsetRun),
Time: timestamppb.New(ct.Clock.Now()),
},
},
},
}}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldHaveLength, 0)
})
Convey("singular group, purge NPR trigger and let dry run continue", func() {
_, pcl := putPCL(33, nprIdx, run.DryRun, ct.Clock.Now())
pm.pb.Pcls[0] = pcl
pcl.PurgeReasons = []*prjpb.PurgeReason{{
ClError: &changelist.CLError{
Kind: &changelist.CLError_OwnerLacksEmail{},
},
ApplyTo: &prjpb.PurgeReason_Triggers{
Triggers: &run.Triggers{
NewPatchsetRunTrigger: pcl.GetTriggers().GetNewPatchsetRunTrigger(),
},
},
}}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldHaveLength, 1)
So(res.RunsToCreate[0].Mode, ShouldEqual, run.DryRun)
})
Convey("combinable group -- obey stabilization_delay", func() {
pm.pb.Pcls[0].ConfigGroupIndexes = []int32{combIdx}
res := mustTriage(oldC)
c := markTriaged(oldC)
c.DecisionTime = timestamppb.New(ct.Clock.Now().Add(stabilizationDelay))
So(res.NewValue, ShouldResembleProto, c)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
ct.Clock.Add(stabilizationDelay * 2)
res = mustTriage(oldC)
c.DecisionTime = nil
So(res.NewValue, ShouldResembleProto, c)
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("NewPatchsetRun combinable group -- ignore stabilization_delay", func() {
_, pcl := putPCL(33, nprCombIdx, run.NewPatchsetRun, ct.Clock.Now())
pm.pb.Pcls[0] = pcl
pcl.PurgeReasons = []*prjpb.PurgeReason{{
ClError: &changelist.CLError{
Kind: &changelist.CLError_UnsupportedMode{},
},
ApplyTo: &prjpb.PurgeReason_Triggers{
Triggers: &run.Triggers{
NewPatchsetRunTrigger: &run.Trigger{
Mode: string(run.NewPatchsetRun),
Time: timestamppb.New(ct.Clock.Now()),
},
},
},
}}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("many groups -- no delay", func() {
// many groups is an error itself
pm.pb.Pcls[0].ConfigGroupIndexes = []int32{singIdx, combIdx, anotherIdx}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldHaveLength, 1)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("with already existing Run:", func() {
_, pcl32 := putPCL(32, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Second))
_, pcl33 := putPCL(33, combIdx, run.FullRun, ct.Clock.Now(), 32)
pm.pb.Pcls = []*prjpb.PCL{pcl32, pcl33}
oldC := &prjpb.Component{Clids: []int64{32, 33}, TriageRequired: true}
ct.Clock.Add(stabilizationDelay)
const expectedRunID = "v8/9042327596854-1-690d9e2cc74b34aa"
Convey("wait a bit if Run is RUNNING", func() {
So(datastore.Put(ctx, &run.Run{ID: expectedRunID, Status: run.Status_RUNNING}), ShouldBeNil)
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime().AsTime(), ShouldResemble, ct.Clock.Now().Add(5*time.Second).UTC())
So(res.RunsToCreate, ShouldBeEmpty)
So(res.CLsToPurge, ShouldBeEmpty)
})
r := &run.Run{
ID: expectedRunID,
Status: run.Status_CANCELLED,
EndTime: datastore.RoundTime(ct.Clock.Now().UTC()),
}
So(datastore.Put(ctx, r), ShouldBeNil)
Convey("wait a bit if Run was just finalized", func() {
ct.Clock.Add(5 * time.Second)
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime().AsTime(), ShouldResemble, r.EndTime.Add(time.Minute).UTC())
So(res.RunsToCreate, ShouldBeEmpty)
So(res.CLsToPurge, ShouldBeEmpty)
})
Convey("purge CLs due to trigger re-use after long-ago finished Run", func() {
ct.Clock.Add(2 * time.Minute)
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
So(res.RunsToCreate, ShouldBeEmpty)
So(res.CLsToPurge, ShouldHaveLength, 2)
for _, p := range res.CLsToPurge {
So(p.GetPurgeReasons(), ShouldHaveLength, 1)
So(p.GetPurgeReasons()[0].GetClError().GetReusedTrigger().GetRun(), ShouldResemble, expectedRunID)
}
})
})
})
Convey("Creates Runs", func() {
Convey("CQVote and NPR triggers", func() {
_, pcl := putPCL(33, nprIdx, run.DryRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldHaveLength, 2)
rc := res.RunsToCreate[0]
So(rc.ConfigGroupID.Name(), ShouldResemble, "newPatchsetRun")
So(rc.Mode, ShouldResemble, run.DryRun)
So(rc.InputCLs, ShouldHaveLength, 1)
rc = res.RunsToCreate[1]
So(rc.InputCLs[0].ID, ShouldEqual, 33)
So(rc.ConfigGroupID.Name(), ShouldResemble, "newPatchsetRun")
So(rc.Mode, ShouldResemble, run.NewPatchsetRun)
So(rc.InputCLs, ShouldHaveLength, 1)
So(rc.InputCLs[0].ID, ShouldEqual, 33)
})
Convey("NPR Only", func() {
Convey("OK", func() {
var pcl *prjpb.PCL
_, pcl = putPCL(33, nprIdx, run.NewPatchsetRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldHaveLength, 1)
rc := res.RunsToCreate[0]
So(rc.ConfigGroupID.Name(), ShouldResemble, "newPatchsetRun")
So(rc.Mode, ShouldResemble, run.NewPatchsetRun)
So(rc.InputCLs, ShouldHaveLength, 1)
So(rc.InputCLs[0].ID, ShouldEqual, 33)
})
Convey("Noop when Run already exists", func() {
var pcl *prjpb.PCL
_, pcl = putPCL(33, nprIdx, run.NewPatchsetRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true, Pruns: makePrunsWithMode(run.NewPatchsetRun, "run-id", 33)}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("EVersion mismatch is an ErrOutdatedPMState", func() {
cl, pcl := putPCL(33, nprIdx, run.NewPatchsetRun, ct.Clock.Now())
cl.EVersion = 2
So(datastore.Put(ctx, cl), ShouldBeNil)
pm.pb.Pcls = []*prjpb.PCL{pcl}
err := failTriage(&prjpb.Component{Clids: []int64{33}, TriageRequired: true})
So(itriager.IsErrOutdatedPMState(err), ShouldBeTrue)
So(err, ShouldErrLike, "EVersion changed 1 => 2")
})
})
Convey("Singular", func() {
Convey("OK", func() {
_, pcl := putPCL(33, singIdx, run.DryRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldHaveLength, 1)
rc := res.RunsToCreate[0]
So(rc.ConfigGroupID.Name(), ShouldResemble, "singular")
So(rc.Mode, ShouldResemble, run.DryRun)
So(rc.InputCLs, ShouldHaveLength, 1)
So(rc.InputCLs[0].ID, ShouldEqual, 33)
})
Convey("Noop when Run already exists", func() {
_, pcl := putPCL(33, singIdx, run.DryRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true, Pruns: makePruns("run-id", 33)}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("EVersion mismatch is an ErrOutdatedPMState", func() {
cl, pcl := putPCL(33, singIdx, run.DryRun, ct.Clock.Now())
cl.EVersion = 2
So(datastore.Put(ctx, cl), ShouldBeNil)
pm.pb.Pcls = []*prjpb.PCL{pcl}
err := failTriage(&prjpb.Component{Clids: []int64{33}, TriageRequired: true})
So(itriager.IsErrOutdatedPMState(err), ShouldBeTrue)
So(err, ShouldErrLike, "EVersion changed 1 => 2")
})
Convey("OK with resolved deps", func() {
_, pcl32 := putPCL(32, singIdx, run.FullRun, ct.Clock.Now())
_, pcl33 := putPCL(33, singIdx, run.DryRun, ct.Clock.Now(), 32)
pm.pb.Pcls = []*prjpb.PCL{pcl32, pcl33}
oldC := &prjpb.Component{Clids: []int64{32, 33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldHaveLength, 2)
sortRunsToCreateByFirstCL(&res)
So(res.RunsToCreate[0].InputCLs[0].ID, ShouldEqual, 32)
So(res.RunsToCreate[0].Mode, ShouldResemble, run.FullRun)
So(res.RunsToCreate[1].InputCLs[0].ID, ShouldEqual, 33)
So(res.RunsToCreate[1].Mode, ShouldResemble, run.DryRun)
})
Convey("OK with existing Runs but on different CLs", func() {
_, pcl31 := putPCL(31, singIdx, run.FullRun, ct.Clock.Now())
_, pcl32 := putPCL(32, singIdx, run.DryRun, ct.Clock.Now(), 31)
_, pcl33 := putPCL(33, singIdx, run.DryRun, ct.Clock.Now(), 32)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl32, pcl33}
oldC := &prjpb.Component{
Clids: []int64{31, 32, 33},
Pruns: makePruns("first", 31, "third", 33),
TriageRequired: true,
}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldHaveLength, 1)
So(res.RunsToCreate[0].InputCLs[0].ID, ShouldEqual, 32)
So(res.RunsToCreate[0].Mode, ShouldResemble, run.DryRun)
})
Convey("Waits for unresolved dep without an error", func() {
pcl32 := &prjpb.PCL{Clid: 32, Eversion: 1, Status: prjpb.PCL_UNKNOWN}
_, pcl33 := putPCL(33, singIdx, run.DryRun, ct.Clock.Now(), 32)
pm.pb.Pcls = []*prjpb.PCL{pcl32, pcl33}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue, ShouldResembleProto, markTriaged(oldC))
// TODO(crbug/1211576): this waiting can last forever. Component needs
// to record how long it has been waiting and abort with clear message
// to the user.
So(res.NewValue.GetDecisionTime(), ShouldBeNil) // wait for external event of loading a dep
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
})
Convey("Combinable", func() {
Convey("OK after obeying stabilization delay", func() {
// Simulate a CL stack <base> -> 31 -> 32 -> 33, which user wants
// to land at the same time by making 31 depend on 33.
_, pcl31 := putPCL(31, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Minute), 33)
_, pcl32 := putPCL(32, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Second), 31)
_, pcl33 := putPCL(33, combIdx, run.FullRun, ct.Clock.Now(), 32, 31)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl32, pcl33}
oldC := &prjpb.Component{Clids: []int64{31, 32, 33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime().AsTime(), ShouldResemble, ct.Clock.Now().Add(stabilizationDelay).UTC())
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
ct.Clock.Add(stabilizationDelay)
oldC = res.NewValue
res = mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
rc := res.RunsToCreate[0]
So(rc.ConfigGroupID.Name(), ShouldResemble, "combinable")
So(rc.Mode, ShouldResemble, run.FullRun)
t := pcl33.GetTriggers().GetCqVoteTrigger()
So(rc.CreateTime, ShouldEqual, t.GetTime().AsTime())
So(rc.InputCLs, ShouldHaveLength, 3)
})
Convey("Even a single CL should wait for stabilization delay", func() {
_, pcl := putPCL(33, combIdx, run.FullRun, ct.Clock.Now())
pm.pb.Pcls = []*prjpb.PCL{pcl}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime().AsTime(), ShouldResemble, ct.Clock.Now().Add(stabilizationDelay).UTC())
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
ct.Clock.Add(stabilizationDelay)
oldC = res.NewValue
res = mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
rc := res.RunsToCreate[0]
So(rc.ConfigGroupID.Name(), ShouldResemble, "combinable")
So(rc.Mode, ShouldResemble, run.FullRun)
So(rc.InputCLs, ShouldHaveLength, 1)
So(rc.InputCLs[0].ID, ShouldEqual, 33)
})
Convey("Waits for unresolved dep, even after stabilization delay", func() {
pcl32 := &prjpb.PCL{Clid: 32, Eversion: 1, Status: prjpb.PCL_UNKNOWN}
_, pcl33 := putPCL(33, combIdx, run.FullRun, ct.Clock.Now(), 32)
pm.pb.Pcls = []*prjpb.PCL{pcl32, pcl33}
oldC := &prjpb.Component{Clids: []int64{33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue.GetDecisionTime().AsTime(), ShouldResemble, ct.Clock.Now().Add(stabilizationDelay).UTC())
So(res.RunsToCreate, ShouldBeEmpty)
ct.Clock.Add(stabilizationDelay)
oldC = res.NewValue
res = mustTriage(oldC)
So(res.NewValue.GetDecisionTime(), ShouldBeNil) // wait for external event of loading a dep
// TODO(crbug/1211576): this waiting can last forever. Component needs
// to record how long it has been waiting and abort with clear message
// to the user.
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("Noop if there is existing Run encompassing all the CLs", func() {
_, pcl31 := putPCL(31, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Hour), 32)
_, pcl32 := putPCL(32, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Hour), 31)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl32}
oldC := &prjpb.Component{Clids: []int64{31, 32}, TriageRequired: true, Pruns: makePruns("runID", 31, 32)}
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
Convey("even if some CLs are no longer triggered", func() {
// Happens during Run abort due to, say, tryjob failure.
pcl31.Triggers = nil
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("even if some CLs are already submitted", func() {
// Happens during Run submission.
pcl32.Triggers = nil
pcl32.Submitted = true
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
})
Convey("Component growing to N+1 CLs that could form a Run while Run on N CLs is already running", func() {
// Simulate scenario of user first uploading 31<-41 and CQing two
// CLs, then much later uploading 51 depending on 31 and CQing 51
// while (31,41) Run is still running.
//
// This may change once postponeExpandingExistingRunScope() function is
// implemented, but for now test documents that CV will just wait
// until (31, 41) Run completes.
_, pcl31 := putPCL(31, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Hour))
_, pcl41 := putPCL(41, combIdx, run.FullRun, ct.Clock.Now().Add(-time.Hour), 31)
_, pcl51 := putPCL(51, combIdx, run.FullRun, ct.Clock.Now(), 31)
ct.Clock.Add(2 * stabilizationDelay)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl41, pcl51}
oldC := &prjpb.Component{Clids: []int64{31, 41, 51}, TriageRequired: true, Pruns: makePruns("41-31", 31, 41)}
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
Convey("Doesn't react to updates that must be handled first by the Run Manager", func() {
_, pcl31 := putPCL(31, combIdx, run.DryRun, ct.Clock.Now().Add(-time.Hour), 31)
_, pcl41 := putPCL(41, combIdx, run.DryRun, ct.Clock.Now().Add(-time.Hour), 31)
_, pcl51 := putPCL(51, combIdx, run.DryRun, ct.Clock.Now().Add(-time.Hour), 31, 41)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl41, pcl51}
oldC := &prjpb.Component{
Clids: []int64{31, 41, 51},
TriageRequired: true,
Pruns: makePruns("sub/mitting", 31, 41, 51),
}
mustWaitForRM := func() {
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
}
Convey("multi-CL Run is being submitted", func() {
pcl31.Submitted = true
pcl31.Triggers = nil
mustWaitForRM()
})
Convey("multi-CL Run is being canceled", func() {
pcl31.Triggers = nil
mustWaitForRM()
})
Convey("multi-CL Run is no longer in the same ConfigGroup", func() {
pcl31.ConfigGroupIndexes = []int32{anotherIdx}
mustWaitForRM()
})
Convey("multi-CL Run is no longer in the same LUCI project", func() {
pcl31.Status = prjpb.PCL_UNWATCHED
pcl31.ConfigGroupIndexes = nil
pcl31.Triggers = nil
mustWaitForRM()
})
})
Convey("Handles races between CL purging and Gerrit -> CLUpdater -> PM state propagation", func() {
// Due to delays / races between purging a CL and PM state,
// it's possible that CL Purger hasn't yet responded with purge end
// result yet PM's view of CL state has changed to look valid, and
// ready to trigger a Run.
// Then, PM must wait for the purge to complete.
_, pcl31 := putPCL(31, combIdx, run.DryRun, ct.Clock.Now())
_, pcl32 := putPCL(32, combIdx, run.DryRun, ct.Clock.Now(), 31)
_, pcl33 := putPCL(33, combIdx, run.DryRun, ct.Clock.Now(), 31)
ct.Clock.Add(2 * stabilizationDelay)
pm.pb.Pcls = []*prjpb.PCL{pcl31, pcl32, pcl33}
pm.pb.PurgingCls = []*prjpb.PurgingCL{
{
Clid: 31, OperationId: "purge-op-31", Deadline: timestamppb.New(ct.Clock.Now().Add(time.Minute)),
ApplyTo: &prjpb.PurgingCL_AllActiveTriggers{AllActiveTriggers: true},
},
}
oldC := &prjpb.Component{Clids: []int64{31, 32, 33}, TriageRequired: true}
res := mustTriage(oldC)
So(res.NewValue.GetTriageRequired(), ShouldBeFalse)
So(res.NewValue.GetDecisionTime(), ShouldBeNil)
So(res.CLsToPurge, ShouldBeEmpty)
So(res.RunsToCreate, ShouldBeEmpty)
})
})
})
})
}
func sortRunsToCreateByFirstCL(res *itriager.Result) {
sort.Slice(res.RunsToCreate, func(i, j int) bool {
return res.RunsToCreate[i].InputCLs[0].ID < res.RunsToCreate[j].InputCLs[0].ID
})
}
// makePruns is readability sugar to create 0+ pruns slice.
// Example use: makePruns("first", 31, 32, "second", 44, "third", 11).
func makePruns(runIDthenCLIDs ...interface{}) []*prjpb.PRun {
return makePrunsWithMode(run.DryRun, runIDthenCLIDs...)
}
func makePrunsWithMode(m run.Mode, runIDthenCLIDs ...interface{}) []*prjpb.PRun {
var out []*prjpb.PRun
var cur *prjpb.PRun
const sentinel = "<$sentinel>"
runIDthenCLIDs = append(runIDthenCLIDs, sentinel)
for _, arg := range runIDthenCLIDs {
switch v := arg.(type) {
case common.RunID:
arg = string(v)
case int:
arg = int64(v)
case common.CLID:
arg = int64(v)
}
switch v := arg.(type) {
case string:
if cur != nil {
if len(cur.GetClids()) == 0 {
panic("two consecutive strings not allowed = each run must have at least one CLID")
}
out = append(out, cur)
}
if v == sentinel {
return out
}
if v == "" {
panic("empty run ID")
}
cur = &prjpb.PRun{Id: string(v), Mode: string(m)}
case int64:
if cur == nil {
panic("CLIDs must follow a string RunID")
}
cur.Clids = append(cur.Clids, v)
}
}
panic("not reachable")
}