blob: 138443191b814d57a9cb4fd3339ffabaaa36cd22 [file] [log] [blame]
// Copyright 2022 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 tryjob
import (
"context"
"math/rand"
"testing"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/cvtesting"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
type mockRN struct {
notifiedRuns common.RunIDs
}
func (r *mockRN) NotifyTryjobsUpdated(ctx context.Context, run common.RunID, _ *TryjobUpdatedEvents) error {
r.notifiedRuns = append(r.notifiedRuns, run)
return nil
}
type returnValues struct {
s Status
r *Result
err error
}
type mockBackend struct {
returns []*returnValues
}
func (b *mockBackend) Update(ctx context.Context, saved *Tryjob) (Status, *Result, error) {
var ret *returnValues
switch len(b.returns) {
case 0:
return 0, nil, nil
case 1:
ret = b.returns[0]
default:
ret = b.returns[0]
b.returns = b.returns[1:]
}
return ret.s, ret.r, ret.err
}
func (b *mockBackend) Kind() string {
return "buildbucket"
}
func makeTryjob(ctx context.Context) (*Tryjob, error) {
return makeTryjobWithStatus(ctx, Status_TRIGGERED)
}
func makeTestRunID(ctx context.Context, someNumber int64) common.RunID {
mockDigest := []byte{
byte(0xff & someNumber),
byte(0xff & (someNumber >> 8)),
byte(0xff & (someNumber >> 16)),
byte(0xff & (someNumber >> 24))}
return common.MakeRunID("test", clock.Now(ctx), 1, mockDigest)
}
func makeTryjobWithStatus(ctx context.Context, status Status) (*Tryjob, error) {
buildID := int64(rand.Int31())
return makeTryjobWithDetails(ctx, buildID, status, makeTestRunID(ctx, buildID), nil)
}
func makeTryjobWithDetails(ctx context.Context, buildID int64, status Status, triggerer common.RunID, reusers common.RunIDs) (*Tryjob, error) {
tj := MustBuildbucketID("cr-buildbucket.example.com", buildID).MustCreateIfNotExists(ctx)
tj.TriggeredBy = triggerer
tj.ReusedBy = reusers
tj.Status = status
return tj, datastore.Put(ctx, tj)
}
func TestHandleTask(t *testing.T) {
Convey("HandleTryjobUpdateTask", t, func() {
ct := cvtesting.Test{}
ctx, clean := ct.SetUp()
defer clean()
rn := &mockRN{}
mb := &mockBackend{}
updater := NewUpdater(ct.TQDispatcher, rn)
updater.RegisterBackend(mb)
Convey("noop", func() {
tryjob, err := makeTryjob(ctx)
So(err, ShouldBeNil)
eid := tryjob.ExternalID
originalEVersion := tryjob.EVersion
mb.returns = []*returnValues{{Status_TRIGGERED, nil, nil}}
So(updater.handleTask(ctx, &UpdateTryjobTask{ExternalId: string(eid)}), ShouldBeNil)
So(rn.notifiedRuns, ShouldHaveLength, 0)
// Reload to ensure no changes took place.
tryjob = eid.MustCreateIfNotExists(ctx)
So(tryjob.EVersion, ShouldEqual, originalEVersion)
})
Convey("succeeds updating", func() {
Convey("status and result", func() {
tryjob, err := makeTryjob(ctx)
So(err, ShouldBeNil)
originalEVersion := tryjob.EVersion
mb.returns = []*returnValues{{Status_ENDED, &Result{Status: Result_SUCCEEDED}, nil}}
Convey("by internal id", func() {
So(updater.handleTask(ctx, &UpdateTryjobTask{
Id: int64(tryjob.ID),
}), ShouldBeNil)
})
Convey("by external id", func() {
So(updater.handleTask(ctx, &UpdateTryjobTask{
ExternalId: string(tryjob.ExternalID),
}), ShouldBeNil)
})
So(rn.notifiedRuns, ShouldHaveLength, 1)
So(rn.notifiedRuns[0], ShouldEqual, tryjob.TriggeredBy)
// Ensure status updated.
tryjob = tryjob.ExternalID.MustCreateIfNotExists(ctx)
So(tryjob.EVersion, ShouldEqual, originalEVersion+1)
So(tryjob.Status, ShouldEqual, Status_ENDED)
So(tryjob.Result.Status, ShouldEqual, Result_SUCCEEDED)
})
Convey("result only", func() {
tryjob, err := makeTryjob(ctx)
So(err, ShouldBeNil)
originalEVersion := tryjob.EVersion
mb.returns = []*returnValues{{Status_TRIGGERED, &Result{Status: Result_SUCCEEDED}, nil}}
So(updater.handleTask(ctx, &UpdateTryjobTask{Id: int64(tryjob.ID)}), ShouldBeNil)
So(rn.notifiedRuns, ShouldHaveLength, 1)
So(rn.notifiedRuns[0], ShouldEqual, tryjob.TriggeredBy)
tryjob = tryjob.ExternalID.MustCreateIfNotExists(ctx)
So(tryjob.EVersion, ShouldEqual, originalEVersion+1)
So(tryjob.Status, ShouldEqual, Status_TRIGGERED)
So(tryjob.Result.Status, ShouldEqual, Result_SUCCEEDED)
})
Convey("status only", func() {
tryjob, err := makeTryjobWithStatus(ctx, Status_PENDING)
So(err, ShouldBeNil)
originalEVersion := tryjob.EVersion
mb.returns = []*returnValues{{Status_TRIGGERED, nil, nil}}
So(updater.handleTask(ctx, &UpdateTryjobTask{Id: int64(tryjob.ID)}), ShouldBeNil)
So(rn.notifiedRuns, ShouldHaveLength, 1)
So(rn.notifiedRuns[0], ShouldEqual, tryjob.TriggeredBy)
tryjob = tryjob.ExternalID.MustCreateIfNotExists(ctx)
So(tryjob.EVersion, ShouldEqual, originalEVersion+1)
So(tryjob.Status, ShouldEqual, Status_TRIGGERED)
So(tryjob.Result, ShouldBeNil)
})
Convey("and notifying triggerer and reuser Runs", func() {
buildID := int64(rand.Int31())
triggerer := makeTestRunID(ctx, buildID)
reusers := common.RunIDs{makeTestRunID(ctx, buildID+100), makeTestRunID(ctx, buildID+200)}
tryjob, err := makeTryjobWithDetails(ctx, buildID, Status_TRIGGERED, triggerer, reusers)
So(err, ShouldBeNil)
mb.returns = []*returnValues{{Status_ENDED, &Result{Status: Result_SUCCEEDED}, nil}}
So(updater.handleTask(ctx, &UpdateTryjobTask{Id: int64(tryjob.ID)}), ShouldBeNil)
// Should have called notifier thrice.
So(rn.notifiedRuns, ShouldHaveLength, 3)
So(rn.notifiedRuns.Equal(common.RunIDs{triggerer, reusers[0], reusers[1]}), ShouldBeTrue)
})
Convey("and notifying reuser Run with no triggerer Run", func() {
buildID := int64(rand.Int31())
reusers := common.RunIDs{makeTestRunID(ctx, buildID+100)}
tryjob, err := makeTryjobWithDetails(ctx, buildID, Status_TRIGGERED, "", reusers)
So(err, ShouldBeNil)
mb.returns = []*returnValues{{Status_ENDED, &Result{Status: Result_SUCCEEDED}, nil}}
So(updater.handleTask(ctx, &UpdateTryjobTask{Id: int64(tryjob.ID)}), ShouldBeNil)
So(rn.notifiedRuns, ShouldHaveLength, 1)
So(rn.notifiedRuns[0], ShouldEqual, reusers[0])
})
})
Convey("fails to", func() {
tryjob, err := makeTryjob(ctx)
So(err, ShouldBeNil)
Convey("update a tryjob with an id that doesn't exist", func() {
So(datastore.Delete(ctx, tryjob), ShouldBeNil)
So(updater.handleTask(ctx, &UpdateTryjobTask{
Id: int64(tryjob.ID),
}), ShouldErrLike, "unknown Tryjob with id")
So(rn.notifiedRuns, ShouldHaveLength, 0)
})
Convey("update a tryjob with an external id that doesn't exist", func() {
So(updater.handleTask(ctx, &UpdateTryjobTask{
ExternalId: string(MustBuildbucketID("does-not-exist.example.com", 1)),
}), ShouldErrLike, "unknown Tryjob with ExternalID")
So(rn.notifiedRuns, ShouldHaveLength, 0)
})
Convey("update a tryjob with neither internal nor external id", func() {
So(updater.handleTask(ctx, &UpdateTryjobTask{}), ShouldErrLike, "expected at least one of {Id, ExternalId}")
So(rn.notifiedRuns, ShouldHaveLength, 0)
})
Convey("update a tryjob with mismatching internal and external ids", func() {
So(updater.handleTask(ctx, &UpdateTryjobTask{
Id: int64(tryjob.ID),
ExternalId: string(MustBuildbucketID("cr-buildbucket.example.com", 1)),
}), ShouldErrLike, "the given internal and external ids for the tryjob do not match")
So(rn.notifiedRuns, ShouldHaveLength, 0)
})
})
})
}