blob: b9527b66a0123ebaab543f975b1da54b997bd386 [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 handler
import (
"fmt"
"testing"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
bbpb "go.chromium.org/luci/buildbucket/proto"
bbutil "go.chromium.org/luci/buildbucket/protoutil"
"go.chromium.org/luci/common/clock"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"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/run"
"go.chromium.org/luci/cv/internal/run/impl/state"
"go.chromium.org/luci/cv/internal/tryjob/requirement"
. "github.com/smartystreets/goconvey/convey"
)
func TestUpdateConfig(t *testing.T) {
Convey("OnCLUpdated", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
const (
lProject = "chromium"
gHost = "x-review.example.com"
gRepoFirst = "repo/first"
gRepoSecond = "repo/second"
gRef = "refs/heads/main"
)
runID := common.MakeRunID(lProject, ct.Clock.Now(), 1, []byte("deadbeef"))
builder := &bbpb.BuilderID{
Project: lProject,
Bucket: "bucket",
Builder: "some-builder",
}
putRunCL := func(ci *gerritpb.ChangeInfo, cg *cfgpb.ConfigGroup) {
triggers := trigger.Find(&trigger.FindInput{ChangeInfo: ci, ConfigGroup: cg})
So(triggers.GetCqVoteTrigger(), ShouldNotBeNil)
rcl := run.RunCL{
Run: datastore.MakeKey(ctx, common.RunKind, string(runID)),
ID: common.CLID(ci.GetNumber()),
ExternalID: changelist.MustGobID(gHost, ci.GetNumber()),
Trigger: triggers.GetCqVoteTrigger(),
Detail: &changelist.Snapshot{
Patchset: ci.GetRevisions()[ci.GetCurrentRevision()].GetNumber(),
Kind: &changelist.Snapshot_Gerrit{
Gerrit: &changelist.Gerrit{
Info: ci,
Host: gHost,
},
},
},
}
So(datastore.Put(ctx, &rcl), ShouldBeNil)
}
// Seed project with one version of prior config.
prjcfgtest.Create(ctx, lProject, &cfgpb.Config{ConfigGroups: []*cfgpb.ConfigGroup{{Name: "ev1"}}})
metaBefore := prjcfgtest.MustExist(ctx, lProject)
So(metaBefore.EVersion, ShouldEqual, 1)
// Set up initial Run state.
cfgCurrent := &cfgpb.Config{
ConfigGroups: []*cfgpb.ConfigGroup{
{
Name: "main",
Gerrit: []*cfgpb.ConfigGroup_Gerrit{{
Url: "https://" + gHost,
Projects: []*cfgpb.ConfigGroup_Gerrit_Project{
{Name: gRepoFirst, RefRegexp: []string{"refs/heads/.+"}},
{Name: gRepoSecond, RefRegexp: []string{"refs/heads/.+"}},
},
}},
Verifiers: &cfgpb.Verifiers{
Tryjob: &cfgpb.Verifiers_Tryjob{
Builders: []*cfgpb.Verifiers_Tryjob_Builder{
{Name: bbutil.FormatBuilderID(builder)},
},
},
},
AdditionalModes: []*cfgpb.Mode{{
Name: "QUICK_DRY_RUN",
CqLabelValue: 1,
TriggeringValue: 1,
TriggeringLabel: "Will-Be-Changed-In-Tests-Below",
}},
},
{
Name: "special",
Gerrit: []*cfgpb.ConfigGroup_Gerrit{{
Url: "https://" + gHost,
Projects: []*cfgpb.ConfigGroup_Gerrit_Project{{
Name: "repo/will-be-replaced-in-tests-below",
RefRegexp: []string{"refs/heads/.+"},
}},
}},
},
},
}
prjcfgtest.Update(ctx, lProject, cfgCurrent)
metaCurrent := prjcfgtest.MustExist(ctx, lProject)
triggerTime := clock.Now(ctx).UTC()
cgMain := cfgCurrent.GetConfigGroups()[0]
putRunCL(gf.CI(1, gf.Project(gRepoFirst), gf.CQ(+1, triggerTime, gf.U("user-1"))), cgMain)
putRunCL(gf.CI(
2, gf.Project(gRepoSecond),
gf.CQ(+1, triggerTime, gf.U("user-1")),
// Quick+1 has no effect as AdditionalModes above is misconfigured.
gf.Vote("Quick", +1, triggerTime, gf.U("user-1")),
), cgMain)
rs := &state.RunState{
Run: run.Run{
ID: runID,
CLs: common.MakeCLIDs(1, 2),
CreateTime: triggerTime,
StartTime: triggerTime.Add(1 * time.Minute),
Status: run.Status_RUNNING,
ConfigGroupID: prjcfgtest.MustExist(ctx, lProject).ConfigGroupIDs[0], // main
Tryjobs: &run.Tryjobs{
RequirementVersion: 1,
RequirementComputedAt: timestamppb.New(triggerTime.Add(1 * time.Minute)),
},
UseCVTryjobExecutor: true,
Mode: run.DryRun,
},
}
runCLs, err := run.LoadRunCLs(ctx, rs.ID, rs.CLs)
So(err, ShouldBeNil)
initialReqmt, err := requirement.Compute(ctx, requirement.Input{
ConfigGroup: cgMain,
RunOwner: rs.Owner,
CLs: runCLs,
RunOptions: rs.Options,
RunMode: rs.Mode,
})
So(err, ShouldBeNil)
So(initialReqmt.OK(), ShouldBeTrue)
rs.Tryjobs.Requirement = initialReqmt.Requirement
// Prepare new config as a copy of existing one. Add extra ConfigGroup to it
// to ensure its hash will always differ.
cfgNew := proto.Clone(cfgCurrent).(*cfgpb.Config)
cfgNew.ConfigGroups = append(cfgNew.ConfigGroups, &cfgpb.ConfigGroup{Name: "foo"})
h, _ := makeTestHandler(&ct)
updateConfig := func() *Result {
prjcfgtest.Update(ctx, lProject, cfgNew)
metaNew := prjcfgtest.MustExist(ctx, lProject)
res, err := h.UpdateConfig(ctx, rs, metaNew.Hash())
So(err, ShouldBeNil)
return res
}
Convey("Noop", func() {
ensureNoop := func(res *Result) {
So(res.State, ShouldEqual, rs)
So(res.SideEffectFn, ShouldBeNil)
So(res.PreserveEvents, ShouldBeFalse)
}
for _, status := range []run.Status{
run.Status_SUCCEEDED,
run.Status_FAILED,
run.Status_CANCELLED,
} {
Convey(fmt.Sprintf("When Run is %s", status), func() {
rs.Status = status
ensureNoop(updateConfig())
})
}
Convey("When given config hash isn't new", func() {
Convey("but is the same as current", func() {
res, err := h.UpdateConfig(ctx, rs, metaCurrent.Hash())
So(err, ShouldBeNil)
ensureNoop(res)
})
Convey("but is older than current", func() {
res, err := h.UpdateConfig(ctx, rs, metaBefore.Hash())
So(err, ShouldBeNil)
ensureNoop(res)
})
})
})
Convey("Preserve events for SUBMITTING Run", func() {
rs.Status = run.Status_SUBMITTING
res := updateConfig()
So(res.State, ShouldEqual, rs)
So(res.SideEffectFn, ShouldBeNil)
So(res.PreserveEvents, ShouldBeTrue)
})
Convey("Upgrades to newer config version when", func() {
ensureUpdated := func(expectedGroupName string) *Result {
res := updateConfig()
So(res.State.ConfigGroupID.Hash(), ShouldNotEqual, metaCurrent.Hash())
So(res.State.ConfigGroupID.Name(), ShouldEqual, expectedGroupName)
So(res.State.Status, ShouldEqual, run.Status_RUNNING)
So(res.State.LogEntries, ShouldHaveLength, 1)
So(res.State.LogEntries[0].GetConfigChanged(), ShouldNotBeNil)
So(res.SideEffectFn, ShouldBeNil)
So(res.PreserveEvents, ShouldBeFalse)
return res
}
Convey("ConfigGroup is same", func() {
ensureUpdated("main")
})
Convey("ConfigGroup renamed", func() {
cfgNew.ConfigGroups[0].Name = "blah"
ensureUpdated("blah")
})
Convey("ConfigGroup re-ordered and renamed", func() {
cfgNew.ConfigGroups[0].Name = "blah"
cfgNew.ConfigGroups[0], cfgNew.ConfigGroups[1] = cfgNew.ConfigGroups[1], cfgNew.ConfigGroups[0]
ensureUpdated("blah")
})
Convey("Verifier config changed", func() {
cfgNew.ConfigGroups[0].Verifiers.TreeStatus = &cfgpb.Verifiers_TreeStatus{Url: "https://whatever.example.com"}
res := ensureUpdated("main")
So(res.State.Tryjobs.GetRequirementVersion(), ShouldEqual, rs.Tryjobs.GetRequirementVersion())
})
Convey("Watched refs changed", func() {
cfgNew.ConfigGroups[0].Gerrit[0].Projects[0].RefRegexpExclude = []string{"refs/heads/exclude"}
ensureUpdated("main")
})
Convey("Tryjob requirement changed", func() {
tryjobVerifier := cfgNew.ConfigGroups[0].Verifiers.Tryjob
tryjobVerifier.Builders = append(tryjobVerifier.Builders,
&cfgpb.Verifiers_Tryjob_Builder{
Name: fmt.Sprintf("%s/another-bucket/another-builder", lProject),
})
res := ensureUpdated("main")
So(proto.Equal(res.State.Tryjobs.GetRequirement(), rs.Tryjobs.GetRequirement()), ShouldBeFalse)
So(res.State.Tryjobs.GetRequirementVersion(), ShouldEqual, rs.Tryjobs.GetRequirementVersion()+1)
So(res.State.Tryjobs.GetRequirementComputedAt().AsTime(), ShouldEqual, ct.Clock.Now().UTC())
})
})
Convey("Cancel Run when", func() {
ensureCancelled := func() {
res := updateConfig()
// Applicable ConfigGroupID should remain the same.
So(res.State.ConfigGroupID, ShouldEqual, rs.ConfigGroupID)
So(res.State.Status, ShouldEqual, run.Status_CANCELLED)
So(res.SideEffectFn, ShouldNotBeNil)
So(res.PreserveEvents, ShouldBeFalse)
}
Convey("a CL is no longer watched", func() {
cfgNew.ConfigGroups[0].Gerrit[0].Projects[1].Name = "repo/different"
ensureCancelled()
})
Convey("a CL is watched by >1 ConfigGroup", func() {
cfgNew.ConfigGroups[1].Gerrit[0].Projects[0].Name = gRepoFirst
ensureCancelled()
})
Convey("CLs are watched by different ConfigGroups", func() {
cfgNew.ConfigGroups[0].Gerrit[0].Projects[0].Name = "repo/different"
cfgNew.ConfigGroups[1].Gerrit[0].Projects[0].Name = gRepoFirst
ensureCancelled()
})
Convey("CLs trigger has changed", func() {
cfgNew.ConfigGroups[0].AdditionalModes[0].TriggeringLabel = "Quick"
ensureCancelled()
})
})
})
}