blob: 3ebdef1fc0428b37a501fa34f25162ae7e500c96 [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 bbfacade
import (
"testing"
"time"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
bbpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/errors"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/cvtesting"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/tryjob"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestLaunch(t *testing.T) {
Convey("Launch", t, func() {
ct := cvtesting.Test{}
ctx, cancel := ct.SetUp()
defer cancel()
f := &Facade{
ClientFactory: ct.BuildbucketFake.NewClientFactory(),
}
const (
gHost = "example-review.googlesource.com"
gRepo = "repo/example"
gChange1 = 11
gChange2 = 22
bbHost = "buildbucket.example.com"
bbHost2 = "buildbucket-2.example.com"
lProject = "testProj"
owner1Email = "owner1@example.com"
owner2Email = "owner2@example.com"
triggerEmail = "triggerer@example.com"
)
builderID := &bbpb.BuilderID{
Project: lProject,
Bucket: "testBucket",
Builder: "testBuilder",
}
ct.BuildbucketFake.AddBuilder(bbHost, builderID, map[string]interface{}{
"foo": "bar",
})
bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, bbHost, lProject)
So(err, ShouldBeNil)
epoch := ct.Clock.Now().UTC()
cl1 := &run.RunCL{
ID: 1,
ExternalID: changelist.MustGobID(gHost, gChange1),
Detail: &changelist.Snapshot{
Patchset: 4,
MinEquivalentPatchset: 3,
Kind: &changelist.Snapshot_Gerrit{
Gerrit: &changelist.Gerrit{
Host: gHost,
Info: &gerritpb.ChangeInfo{
Project: gRepo,
Number: gChange1,
Owner: &gerritpb.AccountInfo{
Email: owner1Email,
},
},
},
},
},
Trigger: &run.Trigger{
Email: triggerEmail,
},
}
cl2 := &run.RunCL{
ID: 2,
ExternalID: changelist.MustGobID(gHost, gChange2),
Detail: &changelist.Snapshot{
Patchset: 10,
MinEquivalentPatchset: 10,
Kind: &changelist.Snapshot_Gerrit{
Gerrit: &changelist.Gerrit{
Host: gHost,
Info: &gerritpb.ChangeInfo{
Project: gRepo,
Number: gChange2,
Owner: &gerritpb.AccountInfo{
Email: owner2Email,
},
},
},
},
},
Trigger: &run.Trigger{
Email: triggerEmail,
},
}
cls := []*run.RunCL{cl1, cl2}
r := &run.Run{
ID: common.MakeRunID(lProject, epoch, 1, []byte("cafe")),
CLs: common.CLIDs{cl1.ID, cl2.ID},
Mode: run.DryRun,
}
Convey("Single Tryjob", func() {
definition := &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: builderID,
},
},
}
Convey("NonExperimental", func() {
tj := &tryjob.Tryjob{
ID: 65535,
Definition: definition,
Status: tryjob.Status_PENDING,
}
err := f.Launch(ctx, []*tryjob.Tryjob{tj}, r, cls)
So(err, ShouldBeNil)
So(tj.ExternalID, ShouldNotBeEmpty)
host, id, err := tj.ExternalID.ParseBuildbucketID()
So(err, ShouldBeNil)
So(host, ShouldEqual, bbHost)
So(tj.Status, ShouldEqual, tryjob.Status_TRIGGERED)
So(tj.Result, ShouldResembleProto, &tryjob.Result{
Status: tryjob.Result_UNKNOWN,
CreateTime: timestamppb.New(ct.Clock.Now()),
UpdateTime: timestamppb.New(ct.Clock.Now()),
Backend: &tryjob.Result_Buildbucket_{
Buildbucket: &tryjob.Result_Buildbucket{
Id: id,
Status: bbpb.Status_SCHEDULED,
Builder: builderID,
},
},
})
build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{
Id: id,
Mask: &bbpb.BuildMask{
AllFields: true,
},
})
So(err, ShouldBeNil)
So(build.GetBuilder(), ShouldResembleProto, builderID)
So(build.GetInput().GetProperties(), ShouldResembleProto, &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewStringValue("bar"),
propertyKey: structpb.NewStructValue(&structpb.Struct{
Fields: map[string]*structpb.Value{
"active": structpb.NewBoolValue(true),
"dryRun": structpb.NewBoolValue(true),
"topLevel": structpb.NewBoolValue(true),
"runMode": structpb.NewStringValue(string(run.DryRun)),
},
}),
},
})
So(build.GetInput().GetGerritChanges(), ShouldResembleProto, []*bbpb.GerritChange{
{Host: gHost, Project: gRepo, Change: gChange1, Patchset: 4},
{Host: gHost, Project: gRepo, Change: gChange2, Patchset: 10},
})
So(build.GetTags(), ShouldResembleProto, []*bbpb.StringPair{
{Key: "cq_attempt_key", Value: "63616665"},
{Key: "cq_cl_group_key", Value: "42497728aa4b5097"},
{Key: "cq_cl_owner", Value: owner1Email},
{Key: "cq_cl_owner", Value: owner2Email},
{Key: "cq_equivalent_cl_group_key", Value: "1aac15146c0bc164"},
{Key: "cq_experimental", Value: "false"},
{Key: "cq_triggerer", Value: triggerEmail},
{Key: "user_agent", Value: "cq"},
})
})
Convey("Experimental Tryjob", func() {
definition.Experimental = true
tj := &tryjob.Tryjob{
ID: 65535,
Definition: definition,
Status: tryjob.Status_PENDING,
}
err := f.Launch(ctx, []*tryjob.Tryjob{tj}, r, cls)
So(err, ShouldBeNil)
So(tj.ExternalID, ShouldNotBeEmpty)
_, id, err := tj.ExternalID.ParseBuildbucketID()
So(err, ShouldBeNil)
build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{
Id: id,
Mask: &bbpb.BuildMask{
AllFields: true,
},
})
So(err, ShouldBeNil)
So(build.GetInput().GetProperties().GetFields()[propertyKey].GetStructValue().GetFields()["experimental"].GetBoolValue(), ShouldBeTrue)
var experimentalTag *bbpb.StringPair
for _, tag := range build.GetTags() {
if tag.GetKey() == "cq_experimental" {
experimentalTag = tag
break
}
}
So(experimentalTag, ShouldNotBeNil)
So(experimentalTag.GetValue(), ShouldEqual, "true")
})
})
Convey("Multiple across hosts", func() {
tryjobs := []*tryjob.Tryjob{
{
ID: 65533,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: &bbpb.BuilderID{
Project: lProject,
Bucket: "bucketFoo",
Builder: "builderFoo",
},
},
},
},
Status: tryjob.Status_PENDING,
},
{
ID: 65534,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: &bbpb.BuilderID{
Project: lProject,
Bucket: "bucketBar",
Builder: "builderBar",
},
},
},
Experimental: true,
},
Status: tryjob.Status_PENDING,
},
{
ID: 65535,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost2,
Builder: &bbpb.BuilderID{
Project: lProject,
Bucket: "bucketBaz",
Builder: "builderBaz",
},
},
},
},
Status: tryjob.Status_PENDING,
},
}
ct.BuildbucketFake.AddBuilder(bbHost, tryjobs[0].Definition.GetBuildbucket().GetBuilder(), nil)
ct.BuildbucketFake.AddBuilder(bbHost, tryjobs[1].Definition.GetBuildbucket().GetBuilder(), nil)
ct.BuildbucketFake.AddBuilder(bbHost2, tryjobs[2].Definition.GetBuildbucket().GetBuilder(), nil)
err := f.Launch(ctx, tryjobs, r, cls)
So(err, ShouldBeNil)
for i, tj := range tryjobs {
So(tj.ExternalID, ShouldNotBeEmpty)
host, id, err := tj.ExternalID.ParseBuildbucketID()
So(err, ShouldBeNil)
switch i {
case 0, 1:
So(host, ShouldEqual, bbHost)
default:
So(host, ShouldEqual, bbHost2)
}
bbClient, err := ct.BuildbucketFake.NewClientFactory().MakeClient(ctx, host, lProject)
So(err, ShouldBeNil)
build, err := bbClient.GetBuild(ctx, &bbpb.GetBuildRequest{
Id: id,
Mask: &bbpb.BuildMask{
AllFields: true,
},
})
So(err, ShouldBeNil)
So(build, ShouldNotBeNil)
}
})
Convey("Failure", func() {
tryjobs := []*tryjob.Tryjob{
{
ID: 65534,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: builderID,
},
},
},
Status: tryjob.Status_PENDING,
},
{
ID: 65535,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: &bbpb.BuilderID{
Project: lProject,
Bucket: "testBucket",
Builder: "anotherBuilder",
},
},
},
},
Status: tryjob.Status_PENDING,
},
}
err := f.Launch(ctx, tryjobs, r, cls)
So(err, ShouldNotBeNil)
So(err, ShouldHaveSameTypeAs, errors.MultiError{})
merrs := err.(errors.MultiError)
So(merrs[0], ShouldBeNil) // First Tryjob launched successfully
So(tryjobs[0].ExternalID, ShouldNotBeEmpty)
So(merrs[1], ShouldBeRPCNotFound)
So(tryjobs[1].ExternalID, ShouldBeEmpty)
})
Convey("Deduplicate", func() {
first := &tryjob.Tryjob{
ID: 655365,
Definition: &tryjob.Definition{
Backend: &tryjob.Definition_Buildbucket_{
Buildbucket: &tryjob.Definition_Buildbucket{
Host: bbHost,
Builder: builderID,
},
},
},
Status: tryjob.Status_PENDING,
}
second := *first // make a copy
err := f.Launch(ctx, []*tryjob.Tryjob{first}, r, cls)
So(err, ShouldBeNil)
ct.Clock.Add(10 * time.Second)
err = f.Launch(ctx, []*tryjob.Tryjob{&second}, r, cls)
So(err, ShouldBeNil)
So(second.ExternalID, ShouldEqual, first.ExternalID)
})
})
}