blob: 03efb8235ba77b4e12e9fb9f57c4925a0d2b56f2 [file] [log] [blame]
// Copyright 2019 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 protoutil
import (
"context"
"fmt"
"sync"
"testing"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc"
"go.chromium.org/luci/common/errors"
pb "go.chromium.org/luci/buildbucket/proto"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
type searchStub struct {
pb.BuildsClient
reqs []*pb.SearchBuildsRequest
mu sync.Mutex
searchBuilds func(context.Context, *pb.SearchBuildsRequest) (*pb.SearchBuildsResponse, error)
}
func (c *searchStub) SearchBuilds(ctx context.Context, in *pb.SearchBuildsRequest, opts ...grpc.CallOption) (*pb.SearchBuildsResponse, error) {
c.mu.Lock()
c.reqs = append(c.reqs, in)
c.mu.Unlock()
return c.searchBuilds(ctx, in)
}
func (c *searchStub) simpleMock(res *pb.SearchBuildsResponse, err error) {
c.searchBuilds = func(context.Context, *pb.SearchBuildsRequest) (*pb.SearchBuildsResponse, error) {
return res, err
}
}
func TestSearch(t *testing.T) {
t.Parallel()
Convey("Search", t, func(c C) {
ctx := context.Background()
client := &searchStub{}
search := func(requests ...*pb.SearchBuildsRequest) ([]*pb.Build, error) {
buildC := make(chan *pb.Build)
errC := make(chan error)
var builds []*pb.Build
go func() {
err := Search(ctx, buildC, client, requests...)
close(buildC)
errC <- err
}()
for b := range buildC {
builds = append(builds, b)
}
return builds, <-errC
}
Convey("One page", func() {
expectedBuilds := []*pb.Build{{Id: 1}, {Id: 2}}
client.simpleMock(&pb.SearchBuildsResponse{Builds: expectedBuilds}, nil)
actualBuilds, err := search(&pb.SearchBuildsRequest{})
So(err, ShouldBeNil)
So(actualBuilds, ShouldResembleProto, expectedBuilds)
})
Convey("Two pages", func() {
client.searchBuilds = func(ctx context.Context, in *pb.SearchBuildsRequest) (*pb.SearchBuildsResponse, error) {
switch in.PageToken {
case "":
return &pb.SearchBuildsResponse{
Builds: []*pb.Build{{Id: 1}, {Id: 2}},
NextPageToken: "token",
}, nil
case "token":
return &pb.SearchBuildsResponse{
Builds: []*pb.Build{{Id: 3}},
}, nil
default:
return nil, errors.Reason("unexpected request").Err()
}
}
actualBuilds, err := search(&pb.SearchBuildsRequest{})
So(err, ShouldBeNil)
So(actualBuilds, ShouldResembleProto, []*pb.Build{{Id: 1}, {Id: 2}, {Id: 3}})
})
Convey("Response error", func() {
client.simpleMock(nil, fmt.Errorf("request failed"))
actualBuilds, err := search(&pb.SearchBuildsRequest{})
So(err, ShouldErrLike, "request failed")
So(actualBuilds, ShouldBeEmpty)
})
Convey("Ensure Required fields", func() {
client.simpleMock(&pb.SearchBuildsResponse{}, nil)
actualBuilds, err := search(&pb.SearchBuildsRequest{
Fields: &field_mask.FieldMask{Paths: []string{"builds.*.created_by"}},
})
So(err, ShouldBeNil)
So(actualBuilds, ShouldBeEmpty)
So(client.reqs, ShouldHaveLength, 1)
So(client.reqs[0].Fields, ShouldResembleProto, &field_mask.FieldMask{Paths: []string{"builds.*.created_by", "builds.*.id", "next_page_token"}})
})
Convey("Interrupt", func() {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
client.simpleMock(&pb.SearchBuildsResponse{
Builds: []*pb.Build{{Id: 1}, {Id: 2}, {Id: 3}},
}, nil)
builds := make(chan *pb.Build)
errC := make(chan error)
go func() {
errC <- Search(ctx, builds, client, &pb.SearchBuildsRequest{})
}()
So(<-builds, ShouldResembleProto, &pb.Build{Id: 1})
cancel()
err := <-errC
So(err, ShouldNotBeNil)
So(err == context.Canceled, ShouldBeTrue)
})
Convey("Multiple requests", func() {
// First stream, with status filter SUCCESS and two pages.
requests := []struct {
status pb.Status
pageToken string
buildIDs []int64
nextPageToken string
}{
// First stream.
{
status: pb.Status_SUCCESS,
buildIDs: []int64{1, 11, 21},
nextPageToken: "1",
},
{
status: pb.Status_SUCCESS,
buildIDs: []int64{31},
pageToken: "1",
},
// Second stream.
{
status: pb.Status_FAILURE,
buildIDs: []int64{2, 12, 22},
nextPageToken: "2",
},
{
status: pb.Status_FAILURE,
buildIDs: []int64{32, 42},
pageToken: "2",
},
// Third stream.
{
status: pb.Status_INFRA_FAILURE,
buildIDs: []int64{3},
},
}
client.searchBuilds = func(ctx context.Context, in *pb.SearchBuildsRequest) (*pb.SearchBuildsResponse, error) {
for _, r := range requests {
if in.PageToken != r.pageToken || in.Predicate.GetStatus() != r.status {
continue
}
builds := make([]*pb.Build, len(r.buildIDs))
for i, id := range r.buildIDs {
builds[i] = &pb.Build{Id: id}
}
return &pb.SearchBuildsResponse{
Builds: builds,
NextPageToken: r.nextPageToken,
}, nil
}
return nil, errors.Reason("unexpected request").Err()
}
builds, err := search([]*pb.SearchBuildsRequest{
{Predicate: &pb.BuildPredicate{Status: pb.Status_SUCCESS}},
{Predicate: &pb.BuildPredicate{Status: pb.Status_FAILURE}},
{Predicate: &pb.BuildPredicate{Status: pb.Status_INFRA_FAILURE}},
}...)
So(err, ShouldBeNil)
So(builds, ShouldResembleProto, []*pb.Build{
{Id: 1},
{Id: 2},
{Id: 3},
{Id: 11},
{Id: 12},
{Id: 21},
{Id: 22},
{Id: 31},
{Id: 32},
{Id: 42},
})
})
Convey("Duplicate build", func() {
client.searchBuilds = func(ctx context.Context, in *pb.SearchBuildsRequest) (*pb.SearchBuildsResponse, error) {
switch {
case in.Predicate.Status == pb.Status_SUCCESS:
return &pb.SearchBuildsResponse{
Builds: []*pb.Build{{Id: 1}, {Id: 11}, {Id: 21}},
}, nil
case in.Predicate.Status == pb.Status_FAILURE:
return &pb.SearchBuildsResponse{
Builds: []*pb.Build{{Id: 2}, {Id: 11}, {Id: 22}},
}, nil
default:
return nil, errors.Reason("unexpected request").Err()
}
}
builds, err := search([]*pb.SearchBuildsRequest{
{Predicate: &pb.BuildPredicate{Status: pb.Status_SUCCESS}},
{Predicate: &pb.BuildPredicate{Status: pb.Status_FAILURE}},
}...)
So(err, ShouldBeNil)
So(builds, ShouldResembleProto, []*pb.Build{
{Id: 1},
{Id: 2},
{Id: 11},
{Id: 21},
{Id: 22},
})
})
Convey("Empty request slice", func() {
actualBuilds, err := search()
So(err, ShouldBeNil)
So(actualBuilds, ShouldBeEmpty)
})
})
}