blob: 3e27b6d87d3b8ed31a299fb1d707be5fc3f87953 [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"
"testing"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock/testclock"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/configs/prjcfg"
"go.chromium.org/luci/cv/internal/cvtesting"
"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 TestDepsTriage(t *testing.T) {
t.Parallel()
Convey("Component's PCL deps triage", t, func() {
// Truncate start time point s.t. easy to see diff in test failures.
epoch := testclock.TestRecentTimeUTC.Truncate(10000 * time.Second)
dryRun := func(t time.Time) *run.Triggers {
return &run.Triggers{CqVoteTrigger: &run.Trigger{Mode: string(run.DryRun), Time: timestamppb.New(t)}}
}
fullRun := func(t time.Time) *run.Triggers {
return &run.Triggers{CqVoteTrigger: &run.Trigger{Mode: string(run.FullRun), Time: timestamppb.New(t)}}
}
sup := &simplePMState{
pb: &prjpb.PState{},
cgs: []*prjcfg.ConfigGroup{
{ID: "hash/singular", Content: &cfgpb.ConfigGroup{}},
{ID: "hash/combinable", Content: &cfgpb.ConfigGroup{CombineCls: &cfgpb.CombineCLs{}}},
{ID: "hash/another", Content: &cfgpb.ConfigGroup{}},
},
}
const singIdx, combIdx, anotherIdx = 0, 1, 2
do := func(pcl *prjpb.PCL, cgIdx int32) *triagedDeps {
backup := prjpb.PState{}
proto.Merge(&backup, sup.pb)
// Actual component doesn't matter in this test.
td := triageDeps(pcl, cgIdx, pmState{sup})
So(sup.pb, ShouldResembleProto, &backup) // must not be modified
return td
}
Convey("Singluar and Combinable behave the same", func() {
sameTests := func(name string, cgIdx int32) {
Convey(name, func() {
Convey("no deps", func() {
sup.pb.Pcls = []*prjpb.PCL{
{Clid: 33, ConfigGroupIndexes: []int32{cgIdx}},
}
td := do(sup.pb.Pcls[0], cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{})
So(td.OK(), ShouldBeTrue)
})
Convey("Valid CL stack CQ+1", func() {
sup.pb.Pcls = []*prjpb.PCL{
{Clid: 31, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(3 * time.Second))},
{Clid: 32, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(2 * time.Second))},
{Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)),
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
}},
}
td := do(sup.PCL(33), cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
})
So(td.OK(), ShouldBeTrue)
})
Convey("Not yet loaded deps", func() {
sup.pb.Pcls = []*prjpb.PCL{
// 31 isn't in PCLs yet
{Clid: 32, Status: prjpb.PCL_UNKNOWN},
{Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)),
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
}},
}
pcl33 := sup.PCL(33)
td := do(pcl33, cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{notYetLoaded: pcl33.GetDeps()})
So(td.OK(), ShouldBeTrue)
})
Convey("Unwatched", func() {
sup.pb.Pcls = []*prjpb.PCL{
{Clid: 31, Status: prjpb.PCL_UNWATCHED},
{Clid: 32, Status: prjpb.PCL_DELETED},
{Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)),
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
}},
}
pcl33 := sup.PCL(33)
td := do(pcl33, cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
invalidDeps: &changelist.CLError_InvalidDeps{
Unwatched: pcl33.GetDeps(),
},
})
So(td.OK(), ShouldBeFalse)
})
Convey("Submitted can be in any config group and they are OK deps", func() {
sup.pb.Pcls = []*prjpb.PCL{
{Clid: 32, ConfigGroupIndexes: []int32{anotherIdx}, Submitted: true},
{Clid: 33, ConfigGroupIndexes: []int32{cgIdx}, Triggers: dryRun(epoch.Add(1 * time.Second)),
Deps: []*changelist.Dep{{Clid: 32, Kind: changelist.DepKind_HARD}}},
}
pcl33 := sup.PCL(33)
td := do(pcl33, cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{submitted: pcl33.GetDeps()})
So(td.OK(), ShouldBeTrue)
})
Convey("Wrong config group", func() {
sup.pb.Pcls = []*prjpb.PCL{
{Clid: 31, Triggers: dryRun(epoch.Add(3 * time.Second)), ConfigGroupIndexes: []int32{anotherIdx}},
{Clid: 32, Triggers: dryRun(epoch.Add(2 * time.Second)), ConfigGroupIndexes: []int32{anotherIdx, cgIdx}},
{Clid: 33, Triggers: dryRun(epoch.Add(1 * time.Second)), ConfigGroupIndexes: []int32{cgIdx},
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
}},
}
pcl33 := sup.PCL(33)
td := do(pcl33, cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
WrongConfigGroup: pcl33.GetDeps(),
},
})
So(td.OK(), ShouldBeFalse)
})
Convey("Too many deps", func() {
// Create maxAllowedDeps+1 deps.
sup.pb.Pcls = make([]*prjpb.PCL, 0, maxAllowedDeps+2)
deps := make([]*changelist.Dep, 0, maxAllowedDeps+1)
for i := 1; i <= maxAllowedDeps+1; i++ {
sup.pb.Pcls = append(sup.pb.Pcls, &prjpb.PCL{
Clid: int64(1000 + i),
ConfigGroupIndexes: []int32{cgIdx},
Triggers: dryRun(epoch.Add(time.Second)),
})
deps = append(deps, &changelist.Dep{Clid: int64(1000 + i), Kind: changelist.DepKind_SOFT})
}
// Add the PCL with the above deps.
sup.pb.Pcls = append(sup.pb.Pcls, &prjpb.PCL{
Clid: 2000,
ConfigGroupIndexes: []int32{cgIdx},
Triggers: dryRun(epoch.Add(time.Second)),
Deps: deps,
})
td := do(sup.PCL(2000), cgIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
TooMany: &changelist.CLError_InvalidDeps_TooMany{
Actual: maxAllowedDeps + 1,
MaxAllowed: maxAllowedDeps,
},
},
})
So(td.OK(), ShouldBeFalse)
})
})
}
sameTests("singular", singIdx)
sameTests("combinable", combIdx)
})
Convey("Singular speciality", func() {
sup.pb.Pcls = []*prjpb.PCL{
{
Clid: 31, ConfigGroupIndexes: []int32{singIdx},
Triggers: dryRun(epoch.Add(3 * time.Second)),
},
{
Clid: 32, ConfigGroupIndexes: []int32{singIdx},
Triggers: fullRun(epoch.Add(2 * time.Second)), // not happy about its dep.
Deps: []*changelist.Dep{{Clid: 31, Kind: changelist.DepKind_HARD}},
},
{
Clid: 33, ConfigGroupIndexes: []int32{singIdx},
Triggers: dryRun(epoch.Add(3 * time.Second)), // doesn't care about deps.
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
},
},
}
Convey("dry run doesn't care about deps' triggers", func() {
pcl33 := sup.PCL(33)
td := do(pcl33, singIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
})
})
Convey("quick dry run doesn't care about deps' triggers", func() {
pcl33 := sup.PCL(33)
pcl33.Triggers.CqVoteTrigger.Mode = string(run.QuickDryRun)
td := do(pcl33, singIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
})
})
Convey("full run doesn't allow any dep by default", func() {
pcl32 := sup.PCL(32)
td := do(pcl32, singIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
SingleFullDeps: pcl32.GetDeps(),
},
})
So(td.OK(), ShouldBeFalse)
Convey("unless allow_submit_with_open_deps is true", func() {
sup.cgs[singIdx].Content.Verifiers = &cfgpb.Verifiers{
GerritCqAbility: &cfgpb.Verifiers_GerritCQAbility{
AllowSubmitWithOpenDeps: true,
},
}
td := do(pcl32, singIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
})
So(td.OK(), ShouldBeTrue)
Convey("but not if dep is soft", func() {
// Soft dependency (ie via Cq-Depend) won't be submitted as part a
// single Submit gerrit RPC, so it can't be allowed.
pcl32.GetDeps()[0].Kind = changelist.DepKind_SOFT
td := do(pcl32, singIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
SingleFullDeps: pcl32.GetDeps(),
},
})
So(td.OK(), ShouldBeFalse)
})
})
})
})
Convey("Combinable speciality", func() {
// Setup valid deps; sub-tests will mutate this to become invalid.
sup.pb.Pcls = []*prjpb.PCL{
{
Clid: 31, ConfigGroupIndexes: []int32{combIdx},
Triggers: dryRun(epoch.Add(3 * time.Second)),
},
{
Clid: 32, ConfigGroupIndexes: []int32{combIdx},
Triggers: dryRun(epoch.Add(2 * time.Second)),
Deps: []*changelist.Dep{{Clid: 31, Kind: changelist.DepKind_HARD}},
},
{
Clid: 33, ConfigGroupIndexes: []int32{combIdx},
Triggers: dryRun(epoch.Add(1 * time.Second)),
Deps: []*changelist.Dep{
{Clid: 31, Kind: changelist.DepKind_SOFT},
{Clid: 32, Kind: changelist.DepKind_HARD},
},
},
}
Convey("dry run expects all deps to be dry", func() {
pcl32 := sup.PCL(32)
Convey("ok", func() {
td := do(pcl32, combIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{lastCQVoteTriggered: epoch.Add(3 * time.Second)})
})
Convey("... not full runs", func() {
// TODO(tandrii): this can and should be supported.
sup.PCL(31).Triggers.CqVoteTrigger.Mode = string(run.FullRun)
td := do(pcl32, combIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
CombinableMismatchedMode: pcl32.GetDeps(),
},
})
})
})
Convey("full run considers any dep incompatible", func() {
pcl33 := sup.PCL(33)
Convey("ok", func() {
for _, pcl := range sup.pb.GetPcls() {
pcl.Triggers.CqVoteTrigger.Mode = string(run.FullRun)
}
td := do(pcl33, combIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{lastCQVoteTriggered: epoch.Add(3 * time.Second)})
})
Convey("... not dry runs", func() {
sup.PCL(32).Triggers.CqVoteTrigger.Mode = string(run.FullRun)
td := do(pcl33, combIdx)
So(td, cvtesting.SafeShouldResemble, &triagedDeps{
lastCQVoteTriggered: epoch.Add(3 * time.Second),
invalidDeps: &changelist.CLError_InvalidDeps{
CombinableMismatchedMode: []*changelist.Dep{{Clid: 32, Kind: changelist.DepKind_HARD}},
},
})
So(td.OK(), ShouldBeFalse)
})
})
})
Convey("iterateNotSubmitted works", func() {
d1 := &changelist.Dep{Clid: 1}
d2 := &changelist.Dep{Clid: 2}
d3 := &changelist.Dep{Clid: 3}
pcl := &prjpb.PCL{}
td := &triagedDeps{}
iterate := func() (out []*changelist.Dep) {
td.iterateNotSubmitted(pcl, func(dep *changelist.Dep) { out = append(out, dep) })
return
}
Convey("no deps", func() {
So(iterate(), ShouldBeEmpty)
})
Convey("only submitted", func() {
td.submitted = []*changelist.Dep{d3, d1, d2}
pcl.Deps = []*changelist.Dep{d3, d1, d2} // order must be the same
So(iterate(), ShouldBeEmpty)
})
Convey("some submitted", func() {
pcl.Deps = []*changelist.Dep{d3, d1, d2}
td.submitted = []*changelist.Dep{d3}
So(iterate(), ShouldResembleProto, []*changelist.Dep{d1, d2})
td.submitted = []*changelist.Dep{d1}
So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d2})
td.submitted = []*changelist.Dep{d2}
So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1})
})
Convey("none submitted", func() {
pcl.Deps = []*changelist.Dep{d3, d1, d2}
So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1, d2})
})
Convey("notYetLoaded deps are iterated over, too", func() {
pcl.Deps = []*changelist.Dep{d3, d1, d2}
td.notYetLoaded = []*changelist.Dep{d3}
td.submitted = []*changelist.Dep{d2}
So(iterate(), ShouldResembleProto, []*changelist.Dep{d3, d1})
})
Convey("panic on invalid usage", func() {
Convey("wrong PCL", func() {
pcl.Deps = []*changelist.Dep{d3, d1, d2}
td.submitted = []*changelist.Dep{d1, d2, d3} // wrong order
So(func() { iterate() }, ShouldPanicLike, fmt.Errorf("(wrong PCL?)"))
})
})
})
})
}