blob: eb62f8df0e562098a2e1cbf21925d2d7dbd4ec24 [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 trigger
import (
"testing"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock/testclock"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
cfgpb "go.chromium.org/luci/cv/api/config/v2"
"go.chromium.org/luci/cv/internal/gerrit/botdata"
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 TestTrigger(t *testing.T) {
t.Parallel()
user1 := gf.U("u-1")
user2 := gf.U("u-2")
user3 := gf.U("u-3")
user4 := gf.U("u-4")
const dryRunVote = 1
const fullRunVote = 2
cg := &cfgpb.ConfigGroup{}
Convey("Find", t, func() {
now := testclock.TestRecentTimeUTC
ci := &gerritpb.ChangeInfo{
Status: gerritpb.ChangeStatus_NEW,
CurrentRevision: "deadbeef~1",
Revisions: map[string]*gerritpb.RevisionInfo{
"deadbeef~1": {
Number: 2,
Created: timestamppb.New(now.Add(-30 * time.Minute)),
},
"deadbeef~2": {
Number: 1,
Created: timestamppb.New(now.Add(-1 * time.Hour)),
},
},
Labels: map[string]*gerritpb.LabelInfo{
CQLabelName: {
All: nil, // set in tests.
},
},
}
Convey("Abandoned CL", func() {
ci.Status = gerritpb.ChangeStatus_ABANDONED
So(Find(ci, cg), ShouldBeNil)
})
Convey("Merged CL", func() {
ci.Status = gerritpb.ChangeStatus_MERGED
So(Find(ci, cg), ShouldBeNil)
})
Convey("No votes", func() {
So(Find(ci, cg), ShouldBeNil)
})
Convey("No Commit-Queue label info", func() {
ci.Labels = nil
So(Find(ci, cg), ShouldBeNil)
})
Convey("Single vote", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
User: user1,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
}}
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-15 * time.Minute)),
Mode: string(run.DryRun),
GerritAccountId: user1.GetAccountId(),
Email: user1.GetEmail(),
})
})
Convey(">CQ+2 is clamped to CQ+2", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
User: user1,
Value: 3,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
}}
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-15 * time.Minute)),
Mode: string(run.FullRun),
GerritAccountId: user1.GetAccountId(),
Email: user1.GetEmail(),
})
})
Convey("Earliest votes wins", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
{
User: user1,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
{
User: user2,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-5 * time.Minute)),
},
}
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-15 * time.Minute)),
Mode: string(run.DryRun),
GerritAccountId: user1.GetAccountId(),
Email: user1.GetEmail(),
})
Convey("except when some later run was canceled via botdata message", func() {
cancelMsg, err := botdata.Append("", botdata.BotData{
Action: botdata.Cancel,
TriggeredAt: now.Add(-15 * time.Minute),
Revision: ci.GetCurrentRevision(),
})
So(err, ShouldBeNil)
ci.Messages = append(ci.Messages, &gerritpb.ChangeMessageInfo{Message: cancelMsg})
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-5 * time.Minute)),
Mode: string(run.DryRun),
GerritAccountId: user2.GetAccountId(),
Email: user2.GetEmail(),
})
})
})
Convey("Earliest CQ+2 vote wins against even earlier CQ+1", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
{
User: user1,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
{
User: user2,
Value: fullRunVote,
Date: timestamppb.New(now.Add(-10 * time.Minute)),
},
{
User: user3,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-5 * time.Minute)),
},
{
User: user4,
Value: fullRunVote,
Date: timestamppb.New(now.Add(-1 * time.Minute)),
},
}
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-10 * time.Minute)),
Mode: string(run.FullRun),
GerritAccountId: user2.GetAccountId(),
Email: user2.GetEmail(),
})
})
Convey("Sticky Vote bumps trigger time to cur revision creation time", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{{
User: user1,
Value: fullRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
}}
ci.CurrentRevision = "deadbeef"
ci.Revisions["deadbeef"] = &gerritpb.RevisionInfo{
Number: 3,
Created: timestamppb.New(now.Add(-10 * time.Minute)),
}
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-10 * time.Minute)),
Mode: string(run.FullRun),
GerritAccountId: user1.GetAccountId(),
Email: user1.GetEmail(),
})
})
Convey("Additional modes", func() {
const quickLabel = "Quick"
cg.AdditionalModes = []*cfgpb.Mode{
{
Name: string(run.QuickDryRun),
CqLabelValue: +1,
TriggeringLabel: quickLabel,
TriggeringValue: +1,
},
}
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
{
User: user1,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
}
ci.Labels[quickLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{
{
User: user1,
Value: +1,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
}}
Convey("Simplest possible QuickDryRun", func() {
trigger := Find(ci, cg)
So(trigger, ShouldResembleProto, &run.Trigger{
Time: timestamppb.New(now.Add(-15 * time.Minute)),
Mode: string(run.QuickDryRun),
GerritAccountId: user1.GetAccountId(),
Email: user1.GetEmail(),
AdditionalLabel: quickLabel,
})
})
Convey("QuickDryRun despite other users' votes", func() {
ci.Labels[CQLabelName].All = []*gerritpb.ApprovalInfo{
{
User: user1,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
{
User: user2,
Value: dryRunVote,
Date: timestamppb.New(now.Add(-10 * time.Minute)),
},
}
ci.Labels[quickLabel] = &gerritpb.LabelInfo{All: []*gerritpb.ApprovalInfo{
{
User: user2,
Value: +2,
Date: timestamppb.New(now.Add(-20 * time.Minute)),
},
{
User: user1,
Value: +1,
Date: timestamppb.New(now.Add(-15 * time.Minute)),
},
}}
trigger := Find(ci, cg)
So(trigger.GetMode(), ShouldEqual, run.QuickDryRun)
})
Convey("Custom mode", func() {
cg.AdditionalModes[0].Name = "CUSTOM_RUN"
trigger := Find(ci, cg)
So(trigger.GetMode(), ShouldEqual, "CUSTOM_RUN")
})
Convey("Not applicable cases", func() {
Convey("Additional vote must have the same timestamp", func() {
Convey("before", func() {
ci.Labels[quickLabel].GetAll()[0].Date.Seconds++
So(Find(ci, cg).GetMode(), ShouldEqual, run.DryRun)
})
Convey("after", func() {
ci.Labels[quickLabel].GetAll()[0].Date.Seconds--
So(Find(ci, cg).GetMode(), ShouldEqual, run.DryRun)
})
})
Convey("Additional vote be from the same account", func() {
ci.Labels[quickLabel].GetAll()[0].User = user2
So(Find(ci, cg).GetMode(), ShouldEqual, run.DryRun)
})
Convey("Additional vote must have expected value", func() {
ci.Labels[quickLabel].GetAll()[0].Value = 100
So(Find(ci, cg).GetMode(), ShouldEqual, run.DryRun)
})
Convey("Additional vote must be for the correct label", func() {
ci.Labels[quickLabel+"-Other"] = ci.Labels[quickLabel]
delete(ci.Labels, quickLabel)
So(Find(ci, cg).GetMode(), ShouldEqual, run.DryRun)
})
Convey("CQ vote must have correct value", func() {
ci.Labels[CQLabelName].GetAll()[0].Value = fullRunVote
So(Find(ci, cg).GetMode(), ShouldEqual, run.FullRun)
})
})
})
})
}