blob: d0e131e8f236ed43aebca8bfdae66ec9fe9abaf8 [file]
package backends
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"go.chromium.org/luci/grpc/appstatus"
"go.skia.org/infra/go/buildbucket"
bpb "go.chromium.org/luci/buildbucket/proto"
spb "google.golang.org/protobuf/types/known/structpb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
const (
TestHost = "chromium-review.googlesource.com"
)
func TestBuildbucketClientConfigs_Defaults_Client(t *testing.T) {
c := DefaultClientConfig()
assert.Equal(t, buildbucket.DEFAULT_HOST, c.Host)
assert.Equal(t, DefaultRetries, c.Retries)
assert.Equal(t, DefaultPerRPCTimeout, c.PerRPCTimeout)
}
func createGerritChange(host, project string, change, patchset int64) *bpb.GerritChange {
return &bpb.GerritChange{
Host: host,
Project: project,
Change: change,
Patchset: patchset,
}
}
func createBuild(id int64, status bpb.Status, endTime *timestamppb.Timestamp, bucket, builder string, patches []*bpb.GerritChange) *bpb.Build {
return &bpb.Build{
Id: id,
Status: status,
// This build finished 31 days from runtime, just outside Cas.
EndTime: endTime,
Builder: &bpb.BuilderID{
Project: ChromeProject,
Bucket: bucket,
Builder: builder,
},
Input: &bpb.Build_Input{
GerritChanges: patches,
},
}
}
func TestCancelBuild_ValidRequest_NoError(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
summary := "no longer needed"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := &bpb.CancelBuildRequest{
Id: buildID,
SummaryMarkdown: summary,
}
mbc.EXPECT().CancelBuild(ctx, req).Return(nil, nil)
err := c.CancelBuild(ctx, buildID, summary)
assert.NoError(t, err)
}
func TestCancelBuild_ValidRequest_Error(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
summary := "no longer needed"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := &bpb.CancelBuildRequest{
Id: buildID,
SummaryMarkdown: summary,
}
resp := appstatus.BadRequest(errors.New("random error"))
mbc.EXPECT().CancelBuild(ctx, req).Return(nil, resp)
err := c.CancelBuild(ctx, buildID, summary)
assert.ErrorContains(t, err, "Failed to cancel build 12345")
}
func TestGetBuildWithPatches_OnePatch_MatchFound(t *testing.T) {
ctx := context.Background()
builder := "builder"
commit := "12345"
c1ps1 := createGerritChange(TestHost, ChromeProject, 54321, 1)
expiredTime := timestamppb.New(time.Now().AddDate(0, -31, 0))
patches := []*bpb.GerritChange{c1ps1}
build1 := createBuild(1, bpb.Status_SUCCESS, expiredTime, DefaultBucket, builder, patches)
build2 := createBuild(2, bpb.Status_STARTED, nil, DefaultBucket, builder, patches)
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(builder, DefaultBucket, commit, patches)
assert.Equal(t, ChromeProject, req.Predicate.Builder.Project)
// response in reverse chronological order.
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
build2,
build1,
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
build, err := c.GetBuildWithPatches(ctx, builder, DefaultBucket, commit, patches)
assert.NoError(t, err)
assert.Equal(t, int64(2), build.GetId())
assert.Equal(t, patches, build.Input.GerritChanges)
}
func TestGetBuildWithPatches_OldBuild_NoMatch(t *testing.T) {
ctx := context.Background()
builder := "builder"
commit := "12345"
c1ps1 := createGerritChange(TestHost, ChromeProject, 54321, 1)
expiredTime := timestamppb.New(time.Now().AddDate(0, -31, 0))
patches := []*bpb.GerritChange{c1ps1}
build1 := createBuild(1, bpb.Status_SUCCESS, expiredTime, DefaultBucket, builder, patches)
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(builder, DefaultBucket, commit, patches)
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
build1,
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
build, err := c.GetBuildWithPatches(ctx, builder, DefaultBucket, commit, patches)
assert.NoError(t, err)
assert.Nil(t, build)
}
func TestGetBuildWithPatches_FailedBuild_NoMatch(t *testing.T) {
ctx := context.Background()
builder := "builder"
commit := "12345"
c1ps1 := createGerritChange(TestHost, ChromeProject, 54321, 1)
patches := []*bpb.GerritChange{c1ps1}
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(builder, DefaultBucket, commit, patches)
// 1 day old
ts := timestamppb.New(time.Now().AddDate(0, 1, 0))
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
createBuild(1, bpb.Status_FAILURE, ts, DefaultBucket, builder, patches),
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
build, err := c.GetBuildWithPatches(ctx, builder, DefaultBucket, commit, patches)
assert.NoError(t, err)
assert.Nil(t, build)
}
func TestGetBuildWithPatches_NilPatches_NoMatch(t *testing.T) {
ctx := context.Background()
builder := "builder"
commit := "12345"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(builder, DefaultBucket, commit, nil)
// 1 day old
ts := timestamppb.New(time.Now().AddDate(0, 1, 0))
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
createBuild(1, bpb.Status_FAILURE, ts, DefaultBucket, builder, nil),
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
build, err := c.GetBuildWithPatches(ctx, builder, DefaultBucket, commit, nil)
assert.NoError(t, err)
assert.Nil(t, build)
}
func TestGetBuildWithDeps_ValidInputs_MatchingBuild(t *testing.T) {
ctx := context.Background()
builder := "builder"
commit := "12345"
webrtc := "https://webrtc.googlesource.com/src"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(builder, DefaultBucket, commit, nil)
depsOverride := map[string]*spb.Value{
// ignored
"foo": {},
// parsed
DepsOverrideKey: {
Kind: &spb.Value_StructValue{
StructValue: &spb.Struct{
Fields: map[string]*spb.Value{
webrtc: {
Kind: &spb.Value_StringValue{
StringValue: "1",
},
},
},
},
},
},
}
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
{
Id: 1,
Status: bpb.Status_SUCCESS,
EndTime: timestamppb.New(time.Now().AddDate(0, 1, 0)),
Builder: &bpb.BuilderID{
Project: ChromeProject,
Bucket: DefaultBucket,
Builder: builder,
},
Input: &bpb.Build_Input{
Properties: &spb.Struct{
Fields: depsOverride,
},
},
},
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
deps := map[string]string{
webrtc: "1",
}
assert.NotNil(t, c.findMatchingBuild(resp.GetBuilds(), deps, nil))
build, err := c.GetBuildWithDeps(ctx, builder, DefaultBucket, commit, deps)
assert.NoError(t, err)
assert.Equal(t, int64(1), build.GetId())
assert.Nil(t, build.Input.GerritChanges)
assert.Equal(t, "1", build.Input.Properties.Fields[DepsOverrideKey].GetStructValue().AsMap()[webrtc].(string))
}
func TestGetBuildFromWaterfall_ExistingMapping_BuildFound(t *testing.T) {
ctx := context.Background()
builder := "Linux Builder Perf"
mirror, _ := PinpointWaterfall[builder]
commit := "12345"
// must be CI bucket, aka WaterfallBucket
ts := timestamppb.New(time.Now().AddDate(0, 1, 0))
build1 := createBuild(1, bpb.Status_SUCCESS, ts, WaterfallBucket, mirror, nil)
build2 := createBuild(2, bpb.Status_STARTED, nil, WaterfallBucket, mirror, nil)
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createSearchBuildRequest(mirror, WaterfallBucket, commit, nil)
// response in reverse chronological order.
resp := &bpb.SearchBuildsResponse{
Builds: []*bpb.Build{
build2,
build1,
},
}
mbc.EXPECT().SearchBuilds(ctx, req).Return(resp, nil)
build, err := c.GetBuildFromWaterfall(ctx, builder, commit)
assert.NoError(t, err)
assert.Equal(t, int64(2), build.GetId())
}
func TestGetBuildFromWaterfall_UnsupportedBuilder_Error(t *testing.T) {
ctx := context.Background()
commit := "12345"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
b, err := c.GetBuildFromWaterfall(ctx, "builder", commit)
assert.ErrorContains(t, err, "has no supported CI waterfall builder")
assert.Nil(t, b)
}
func TestGetBuildStatus_ValidRequest_StatusStarted(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := &bpb.GetBuildStatusRequest{
Id: buildID,
}
resp := createBuild(buildID, bpb.Status_STARTED, nil, "try", "builder", nil)
mbc.EXPECT().GetBuildStatus(ctx, req).Return(resp, nil)
status, err := c.GetBuildStatus(ctx, buildID)
assert.NoError(t, err)
assert.Equal(t, bpb.Status_STARTED, status)
}
func TestGetBuildStatus_Error_BuildStatusUnspecified(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := &bpb.GetBuildStatusRequest{
Id: buildID,
}
resp := appstatus.BadRequest(errors.New("random error"))
mbc.EXPECT().GetBuildStatus(ctx, req).Return(nil, resp)
status, err := c.GetBuildStatus(ctx, buildID)
assert.ErrorContains(t, err, "random error")
assert.Equal(t, bpb.Status_STATUS_UNSPECIFIED, status)
}
func createCASResponse(buildID int64, status bpb.Status, target, hash string) *bpb.Build {
return &bpb.Build{
Id: buildID,
Status: status,
Output: &bpb.Build_Output{
Properties: &spb.Struct{
Fields: map[string]*spb.Value{
// ignored
"foo": {},
// parsed
"swarm_hashes_refs/refs/head/main/without_patch": {
Kind: &spb.Value_StructValue{
StructValue: &spb.Struct{
Fields: map[string]*spb.Value{
target: {
Kind: &spb.Value_StringValue{
StringValue: hash,
},
},
},
},
},
},
},
},
},
}
}
func TestGetCASReference_ValidRequest_CASResponse(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
target := "performance_test_suite"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createCASReferenceRequest(buildID)
assert.Equal(t, "output.properties", req.Mask.Fields.Paths[0])
resp := createCASResponse(buildID, bpb.Status_SUCCESS, target, "somehash/123")
mbc.EXPECT().GetBuild(ctx, req).Return(resp, nil)
ref, err := c.GetCASReference(ctx, buildID, target)
assert.NoError(t, err)
assert.Equal(t, DefaultCASInstance, ref.CasInstance)
assert.Equal(t, "somehash", ref.Digest.Hash)
assert.Equal(t, int64(123), ref.Digest.SizeBytes)
}
func TestGetCASReference_BuildInProgress_Error(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
target := "performance_test_suite"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createCASReferenceRequest(buildID)
resp := &bpb.Build{
Id: buildID,
Status: bpb.Status_STARTED,
}
mbc.EXPECT().GetBuild(ctx, req).Return(resp, nil)
ref, err := c.GetCASReference(ctx, buildID, target)
assert.ErrorContains(t, err, "Cannot fetch CAS information from build 12345 with status STARTED")
assert.Nil(t, ref)
}
func TestGetCASReference_MissingTargetInResponse_Error(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
target := "performance_test_suite"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createCASReferenceRequest(buildID)
resp := createCASResponse(buildID, bpb.Status_SUCCESS, "other_test_suite", "somehash/123")
mbc.EXPECT().GetBuild(ctx, req).Return(resp, nil)
ref, err := c.GetCASReference(ctx, buildID, target)
assert.ErrorContains(t, err, "The target performance_test_suite cannot be found in the output properties")
assert.Nil(t, ref)
}
func TestGetCASReference_ModifiedCasHashFormat_Error(t *testing.T) {
ctx := context.Background()
buildID := int64(12345)
target := "performance_test_suite"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
req := c.createCASReferenceRequest(buildID)
wrongHash := "somehash/123/456"
resp := createCASResponse(buildID, bpb.Status_SUCCESS, target, wrongHash)
mbc.EXPECT().GetBuild(ctx, req).Return(resp, nil)
ref, err := c.GetCASReference(ctx, buildID, target)
assert.ErrorContains(t, err, fmt.Sprintf("CAS hash %s has been changed to an unparsable format", wrongHash))
assert.Nil(t, ref)
}
func TestStartChromeBuild_BuildWithDeps_SuccessfullyScheduled(t *testing.T) {
ctx := context.Background()
builder := "Linux Builder Perf"
commit := "12345"
ctl := gomock.NewController(t)
defer ctl.Finish()
mbc := bpb.NewMockBuildsClient(ctl)
c := NewBuildbucketClient(mbc)
webrtc := "https://webrtc.googlesource.com/src"
depsMap := map[string]string{
webrtc: "1",
}
req := c.createChromeBuildRequest("1", "1", builder, commit, depsMap, nil)
// Checking defaults
assert.Equal(t, ChromeProject, req.Builder.Project)
assert.Equal(t, DefaultBucket, req.Builder.Bucket)
assert.Equal(t, ChromiumGitilesHost, req.GitilesCommit.Host)
assert.Equal(t, ChromiumGitilesProject, req.GitilesCommit.Project)
assert.Equal(t, ChromiumGitilesRefAtHead, req.GitilesCommit.Ref)
resp := &bpb.Build{
Id: int64(12345),
Status: bpb.Status_SCHEDULED,
}
mbc.EXPECT().ScheduleBuild(gomock.AssignableToTypeOf(ctx), req).Return(resp, nil)
build, err := c.StartChromeBuild(ctx, "1", "1", builder, commit, depsMap, nil)
assert.NoError(t, err)
assert.Equal(t, bpb.Status_SCHEDULED, build.Status)
}