blob: 541befe9b4eb25cf2924732927d68f7cf0d1021c [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 runcreator
import (
"encoding/hex"
"fmt"
"testing"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/auth/identity"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/common/retry/transient"
"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"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/gerrit"
gf "go.chromium.org/luci/cv/internal/gerrit/gerritfake"
"go.chromium.org/luci/cv/internal/gerrit/trigger"
"go.chromium.org/luci/cv/internal/prjmanager"
"go.chromium.org/luci/cv/internal/prjmanager/pmtest"
"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/run/eventpb"
"go.chromium.org/luci/cv/internal/run/runtest"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestComputeCLsDigest(t *testing.T) {
t.Parallel()
Convey("RunBuilder.computeCLsDigest works", t, func() {
// This test mirrors the `test_attempt_key_hash` in CQDaemon's
// pending_manager/test/gerrit_test.py file.
snapshotOf := func(host string, num int64, rev string) *changelist.Snapshot {
return &changelist.Snapshot{
Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{
Host: host,
Info: &gerritpb.ChangeInfo{Number: num, CurrentRevision: rev},
}},
}
}
epoch := time.Date(2020, time.December, 31, 0, 0, 0, 0, time.UTC)
triggerAt := func(mode run.Mode, account int64, delay time.Duration) *run.Trigger {
return &run.Trigger{
Time: timestamppb.New(epoch.Add(delay)),
Mode: string(mode),
GerritAccountId: account,
}
}
rb := Creator{
InputCLs: []CL{
{
Snapshot: snapshotOf("x-review.example.com", 1234567, "rev2"),
TriggerInfo: triggerAt(run.FullRun, 006, 49999*time.Microsecond),
},
{
Snapshot: snapshotOf("y-review.example.com", 7654321, "rev3"),
TriggerInfo: triggerAt(run.FullRun, 007, 777777*time.Microsecond),
},
},
}
rb.computeCLsDigest()
So(rb.runIDBuilder.version, ShouldEqual, 1)
So(hex.EncodeToString(rb.runIDBuilder.digest), ShouldEqual, "bc86ed248de55fb0")
// The CLsDigest must be agnostic of input CLs order.
rb2 := Creator{InputCLs: []CL{rb.InputCLs[1], rb.InputCLs[0]}}
rb2.computeCLsDigest()
So(hex.EncodeToString(rb2.runIDBuilder.digest), ShouldEqual, "bc86ed248de55fb0")
})
}
func TestRunBuilder(t *testing.T) {
t.Parallel()
Convey("RunBuilder works", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
pmNotifier := prjmanager.NewNotifier(ct.TQDispatcher)
runNotifier := run.NewNotifier(ct.TQDispatcher)
clMutator := changelist.NewMutator(ct.TQDispatcher, pmNotifier, runNotifier)
const lProject = "infra"
const gHost = "x-review.example.com"
const gProject = "infra/luci/luci-go"
makeCI := func(n int) *gerritpb.ChangeInfo {
votedAt := ct.Clock.Now()
return gf.CI(n,
gf.Project(gProject),
gf.CQ(1, votedAt, gf.U("user-1")),
gf.Updated(votedAt),
)
}
makeSnapshot := func(ci *gerritpb.ChangeInfo) *changelist.Snapshot {
min, cur, err := gerrit.EquivalentPatchsetRange(ci)
So(err, ShouldBeNil)
return &changelist.Snapshot{
Kind: &changelist.Snapshot_Gerrit{Gerrit: &changelist.Gerrit{
Host: gHost,
Info: ci,
}},
LuciProject: lProject,
ExternalUpdateTime: ci.GetUpdated(),
Patchset: int32(cur),
MinEquivalentPatchset: int32(min),
}
}
writeCL := func(snapshot *changelist.Snapshot) *changelist.CL {
eid := changelist.MustGobID(snapshot.GetGerrit().GetHost(), snapshot.GetGerrit().GetInfo().GetNumber())
cl := eid.MustCreateIfNotExists(ctx)
cl.Snapshot = snapshot
So(datastore.Put(ctx, cl), ShouldBeNil)
return cl
}
triggerOf := func(cl *changelist.CL) *run.Trigger {
t := trigger.Find(cl.Snapshot.GetGerrit().GetInfo(), &cfgpb.ConfigGroup{})
So(t, ShouldNotBeNil)
return t
}
ct.Clock.Add(time.Minute)
cl1 := writeCL(makeSnapshot(makeCI(1)))
ct.Clock.Add(time.Minute)
cl2 := writeCL(makeSnapshot(makeCI(2)))
cl2.IncompleteRuns = common.MakeRunIDs("expected/000-run")
So(datastore.Put(ctx, cl2), ShouldBeNil)
owner, err := identity.MakeIdentity("user:owner@example.com")
So(err, ShouldBeNil)
rb := Creator{
LUCIProject: lProject,
ConfigGroupID: prjcfg.ConfigGroupID("sha256:cafe/cq-group"),
OperationID: "this-operation-id",
Mode: run.DryRun,
Owner: owner,
Options: &run.Options{},
ExpectedIncompleteRunIDs: common.MakeRunIDs("expected/000-run"),
InputCLs: []CL{
{
ID: cl1.ID,
TriggerInfo: triggerOf(cl1),
ExpectedEVersion: 1,
Snapshot: cl1.Snapshot,
},
{
ID: cl2.ID,
ExpectedEVersion: 1,
TriggerInfo: triggerOf(cl2),
Snapshot: cl2.Snapshot,
},
},
}
projectStateOffload := &prjmanager.ProjectStateOffload{
Project: datastore.MakeKey(ctx, prjmanager.ProjectKind, rb.LUCIProject),
ConfigHash: "sha256:cafe",
Status: prjpb.Status_STARTED,
}
So(datastore.Put(ctx, projectStateOffload), ShouldBeNil)
Convey("Checks preconditions", func() {
Convey("No ProjectStateOffload", func() {
So(datastore.Delete(ctx, projectStateOffload), ShouldBeNil)
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, "failed to load ProjectStateOffload")
So(StateChangedTag.In(err), ShouldBeFalse)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("Mismatched project status", func() {
projectStateOffload.Status = prjpb.Status_STOPPING
So(datastore.Put(ctx, projectStateOffload), ShouldBeNil)
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, "status is STOPPING, expected STARTED")
So(StateChangedTag.In(err), ShouldBeTrue)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("Mismatched project config", func() {
projectStateOffload.ConfigHash = "wrong-hash"
So(datastore.Put(ctx, projectStateOffload), ShouldBeNil)
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, "expected sha256:cafe")
So(StateChangedTag.In(err), ShouldBeTrue)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("CL not exists", func() {
So(datastore.Delete(ctx, cl2), ShouldBeNil)
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, fmt.Sprintf("CL %d doesn't exist", cl2.ID))
So(StateChangedTag.In(err), ShouldBeFalse)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("Mismatched CL version", func() {
rb.InputCLs[0].ExpectedEVersion = 11
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, fmt.Sprintf("CL %d changed since EVersion 11", cl1.ID))
So(StateChangedTag.In(err), ShouldBeTrue)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("Unexpected IncompleteRun in a CL", func() {
cl2.IncompleteRuns = common.MakeRunIDs("unexpected/111-run")
So(datastore.Put(ctx, cl2), ShouldBeNil)
_, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, fmt.Sprintf(`CL %d has unexpected incomplete runs: [unexpected/111-run]`, cl2.ID))
So(StateChangedTag.In(err), ShouldBeTrue)
So(transient.Tag.In(err), ShouldBeFalse)
})
})
const expectedRunID = "infra/9042331276854-1-afc7c13288093a6d"
Convey("First test to fail: check ID assumption", func() {
// If this test fails due to change of runID scheme, update the constant
// above.
rb.prepare(ct.Clock.Now())
So(rb.runID, ShouldEqual, expectedRunID)
})
Convey("Run already created", func() {
Convey("by someone else", func() {
err := datastore.Put(ctx, &run.Run{
ID: expectedRunID,
CreationOperationID: "concurrent runner",
})
So(err, ShouldBeNil)
_, err = rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldErrLike, `already created with OperationID "concurrent runner"`)
So(StateChangedTag.In(err), ShouldBeFalse)
So(transient.Tag.In(err), ShouldBeFalse)
})
Convey("by us", func() {
err := datastore.Put(ctx, &run.Run{
ID: expectedRunID,
CreationOperationID: rb.OperationID,
})
So(err, ShouldBeNil)
r, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldBeNil)
So(r, ShouldNotBeNil)
})
})
Convey("ExpectedRunID works if CreateTime is given", func() {
rb.CreateTime = ct.Clock.Now()
// For realism and to prevent non-determinism in production,
// make CreateTime in the past.
ct.Clock.Add(time.Second)
So(rb.ExpectedRunID(), ShouldResemble, common.RunID(expectedRunID))
})
Convey("ExpectedRunID panics if CreateTime is not given", func() {
rb.CreateTime = time.Time{}
So(func() { rb.ExpectedRunID() }, ShouldPanic)
})
Convey("New Run is created", func() {
r, err := rb.Create(ctx, clMutator, pmNotifier, runNotifier)
So(err, ShouldBeNil)
expectedRun := &run.Run{
ID: expectedRunID,
CQDAttemptKey: "afc7c13288093a6d",
EVersion: 1,
CreateTime: datastore.RoundTime(ct.Clock.Now().UTC()),
UpdateTime: datastore.RoundTime(ct.Clock.Now().UTC()),
CLs: common.CLIDs{cl1.ID, cl2.ID},
Status: run.Status_PENDING,
CreationOperationID: rb.run.CreationOperationID,
ConfigGroupID: rb.ConfigGroupID,
Mode: rb.Mode,
Owner: rb.Owner,
Options: &run.Options{},
}
So(r, runtest.ShouldResembleRun, expectedRun)
for i, cl := range rb.cls {
So(cl.EVersion, ShouldEqual, rb.InputCLs[i].ExpectedEVersion+1)
So(cl.UpdateTime, ShouldResemble, r.CreateTime)
}
// Run is properly saved
saved := &run.Run{ID: expectedRun.ID}
So(datastore.Get(ctx, saved), ShouldBeNil)
So(saved, runtest.ShouldResembleRun, expectedRun)
for i := range rb.InputCLs {
i := i
Convey(fmt.Sprintf("RunCL %d-th is properly saved", i), func() {
saved := &run.RunCL{
ID: rb.InputCLs[i].ID,
Run: datastore.MakeKey(ctx, run.RunKind, expectedRunID),
}
So(datastore.Get(ctx, saved), ShouldBeNil)
So(saved.ExternalID, ShouldEqual, rb.cls[i].ExternalID)
So(saved.Trigger, ShouldResembleProto, rb.InputCLs[i].TriggerInfo)
So(saved.Detail, ShouldResembleProto, rb.cls[i].Snapshot)
})
Convey(fmt.Sprintf("CL %d-th is properly updated", i), func() {
saved := &changelist.CL{ID: rb.InputCLs[i].ID}
So(datastore.Get(ctx, saved), ShouldBeNil)
So(saved.IncompleteRuns.ContainsSorted(expectedRunID), ShouldBeTrue)
So(saved.UpdateTime, ShouldResemble, expectedRun.UpdateTime)
So(saved.EVersion, ShouldEqual, rb.InputCLs[i].ExpectedEVersion+1)
})
}
// RunLog must contain the first entry for Creation.
entries, err := run.LoadRunLogEntries(ctx, expectedRun.ID)
So(err, ShouldBeNil)
So(entries, ShouldHaveLength, 1)
So(entries[0].GetCreated().GetConfigGroupId(), ShouldResemble, string(expectedRun.ConfigGroupID))
// Both PM and RM must be notified about new Run.
pmtest.AssertInEventbox(ctx, lProject, &prjpb.Event{Event: &prjpb.Event_RunCreated{RunCreated: &prjpb.RunCreated{
RunId: string(r.ID),
}}})
runtest.AssertInEventbox(ctx, r.ID, &eventpb.Event{Event: &eventpb.Event_Start{Start: &eventpb.Start{}}})
// RM must have an immediate task to start working on a new Run.
So(runtest.Runs(ct.TQ.Tasks()), ShouldResemble, common.RunIDs{r.ID})
})
})
}