go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/list_test.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gerrit
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/logging/gologger"
    30  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  	. "go.chromium.org/luci/common/testing/assertions"
    34  )
    35  
    36  func TestSimpleDeduper(t *testing.T) {
    37  	t.Parallel()
    38  
    39  	Convey("listChangesDeduper works", t, func() {
    40  		epoch := time.Date(2020, time.February, 3, 10, 30, 0, 0, time.UTC)
    41  		ci := func(i int64, t time.Time) *gerritpb.ChangeInfo {
    42  			return &gerritpb.ChangeInfo{
    43  				Number:  i,
    44  				Updated: timestamppb.New(t),
    45  			}
    46  		}
    47  		l := listChangesDeduper{}
    48  
    49  		a := []*gerritpb.ChangeInfo{
    50  			ci(2, epoch.Add(time.Minute)),
    51  			ci(1, epoch.Add(time.Second)),
    52  		}
    53  		So(l.appendSorted(nil, a), ShouldResembleProto, a)
    54  		So(l.mergeSorted(a, a), ShouldResembleProto, a)
    55  
    56  		b := []*gerritpb.ChangeInfo{
    57  			ci(3, epoch.Add(time.Minute+time.Second)),
    58  			ci(1, epoch.Add(time.Minute)),
    59  			ci(4, epoch.Add(time.Second)),
    60  			ci(5, epoch.Add(time.Second)),
    61  		}
    62  		c := l.mergeSorted(a, b)
    63  		So(c, ShouldResembleProto, []*gerritpb.ChangeInfo{
    64  			ci(3, epoch.Add(time.Minute+time.Second)),
    65  			ci(1, epoch.Add(time.Minute)),
    66  			ci(2, epoch.Add(time.Minute)),
    67  			ci(4, epoch.Add(time.Second)),
    68  			ci(5, epoch.Add(time.Second)),
    69  		})
    70  
    71  		So(l.appendSorted(c, []*gerritpb.ChangeInfo{
    72  			ci(4, epoch.Add(time.Second)),
    73  			ci(5, epoch.Add(time.Second)),
    74  			ci(6, epoch.Add(time.Second)),
    75  			ci(7, epoch.Add(time.Millisecond)),
    76  		}), ShouldResembleProto, []*gerritpb.ChangeInfo{
    77  			ci(3, epoch.Add(time.Minute+time.Second)),
    78  			ci(1, epoch.Add(time.Minute)),
    79  			ci(2, epoch.Add(time.Minute)),
    80  			ci(4, epoch.Add(time.Second)),
    81  			ci(5, epoch.Add(time.Second)),
    82  			ci(6, epoch.Add(time.Second)),
    83  			ci(7, epoch.Add(time.Millisecond)),
    84  		})
    85  	})
    86  }
    87  
    88  func TestPagingListChanges(t *testing.T) {
    89  	t.Parallel()
    90  
    91  	Convey("PagingListChanges works", t, func() {
    92  		ctx := context.Background()
    93  		if testing.Verbose() {
    94  			ctx = logging.SetLevel(gologger.StdConfig.Use(ctx), logging.Debug)
    95  		}
    96  		now := time.Date(2020, time.February, 3, 10, 30, 0, 0, time.UTC)
    97  		makeCI := func(i int64, age time.Duration) *gerritpb.ChangeInfo {
    98  			return &gerritpb.ChangeInfo{
    99  				Number:  i,
   100  				Updated: timestamppb.New(now.Add(-age)),
   101  			}
   102  		}
   103  		const more = true
   104  		const noMore = false
   105  		makeResp := func(moreChanges bool, cs ...*gerritpb.ChangeInfo) *gerritpb.ListChangesResponse {
   106  			return &gerritpb.ListChangesResponse{MoreChanges: moreChanges, Changes: cs}
   107  		}
   108  
   109  		fake := fakeListChanges{}
   110  		pager := listChangesPager{
   111  			client: &fake,
   112  			req: &gerritpb.ListChangesRequest{
   113  				Query: "status:new",
   114  			},
   115  			opts: PagingListChangesOptions{
   116  				Limit: 100, // required
   117  			},
   118  		}
   119  
   120  		Convey("Happy 1 RPC path", func() {
   121  			pager.opts.Limit = 1
   122  			pager.opts.PageSize = 100
   123  			fake.reset(makeResp(noMore, makeCI(1, time.Second), makeCI(2, 2*time.Second), makeCI(3, 3*time.Second)))
   124  			resp, err := pager.pagingListChanges(ctx)
   125  			So(err, ShouldBeNil)
   126  			So(resp, ShouldResembleProto, makeResp(more, makeCI(1, time.Second)))
   127  			So(fake.calls, ShouldResembleProto, []*gerritpb.ListChangesRequest{
   128  				{Query: "status:new", Limit: 100},
   129  			})
   130  		})
   131  
   132  		Convey("Propagates request and grpc opts", func() {
   133  			pager.opts.Limit = 2
   134  			pager.req.Options = []gerritpb.QueryOption{gerritpb.QueryOption_LABELS}
   135  			pager.grpcOpts = []grpc.CallOption{grpc.MaxCallSendMsgSize(1)}
   136  
   137  			fake.expectGrpcOpts = []grpc.CallOption{grpc.MaxCallSendMsgSize(1)}
   138  			fake.reset(makeResp(noMore, makeCI(1, time.Second)))
   139  
   140  			resp, err := pager.pagingListChanges(ctx)
   141  			So(err, ShouldBeNil)
   142  			So(resp, ShouldResembleProto, makeResp(noMore, makeCI(1, time.Second)))
   143  			So(fake.calls, ShouldResembleProto, []*gerritpb.ListChangesRequest{
   144  				{
   145  					Query:   "status:new",
   146  					Options: []gerritpb.QueryOption{gerritpb.QueryOption_LABELS},
   147  					Limit:   100,
   148  					Offset:  0,
   149  				},
   150  			})
   151  		})
   152  
   153  		Convey("Empty response is trusted", func() {
   154  			fake.reset(makeResp(noMore))
   155  			resp, err := pager.pagingListChanges(ctx)
   156  			So(err, ShouldBeNil)
   157  			So(resp, ShouldResembleProto, makeResp(noMore))
   158  		})
   159  
   160  		Convey("Doesn't trust MoreChanges=false", func() {
   161  			pager.opts.Limit = 3
   162  			pager.opts.PageSize = 2
   163  			pager.opts.MoreChangesTrustFactor = 0.9
   164  			fake.reset(
   165  				// false should not be trusted here...
   166  				makeResp(noMore, makeCI(1, 1*time.Second), makeCI(2, 2*time.Second), makeCI(3, 3*time.Second)),
   167  				// ... so even older ones are checked, and now it's trusted.
   168  				makeResp(noMore, makeCI(3, 3*time.Second)),
   169  				// finally, check for newer changes, but there is still just #1.
   170  				makeResp(noMore, makeCI(1, 1*time.Second)),
   171  			)
   172  			resp, err := pager.pagingListChanges(ctx)
   173  			So(err, ShouldBeNil)
   174  			So(resp, ShouldResembleProto, makeResp(noMore,
   175  				makeCI(1, 1*time.Second),
   176  				makeCI(2, 2*time.Second),
   177  				makeCI(3, 3*time.Second),
   178  			))
   179  			// Early fail if initial setup changes, which would also break fake.calls
   180  			// assertion below, which should speed up the debugging time.
   181  			So(FormatTime(now), ShouldEqual, `"2020-02-03 10:30:00.000000000"`)
   182  			So(fake.calls, ShouldResembleProto, []*gerritpb.ListChangesRequest{
   183  				{
   184  					Query: "status:new",
   185  					Limit: 2,
   186  				},
   187  				{
   188  					Query: `status:new before:"2020-02-03 10:29:57.000000000"`, // before:#3.Updated
   189  					Limit: 2,
   190  				},
   191  				{
   192  					Query: `status:new after:"2020-02-03 10:29:59.000000000"`, // after:#1.Updated.
   193  					Limit: 2,
   194  				},
   195  			})
   196  		})
   197  
   198  		Convey("Avoid misses due to racy updates", func() {
   199  			pager.opts.Limit = 4
   200  			pager.opts.PageSize = 3
   201  			fake.reset(
   202  				makeResp(more, makeCI(1, 1*time.Second), makeCI(2, 2*time.Second), makeCI(3, 3*time.Second)),
   203  				// Simulate #4 getting updated concurrently, so it's missing from olders
   204  				// changes:
   205  				makeResp(noMore, makeCI(3, 3*time.Second), makeCI(5, 5*time.Second)),
   206  				makeResp(noMore, makeCI(4, 4*time.Millisecond), makeCI(1, 1*time.Second)),
   207  			)
   208  			resp, err := pager.pagingListChanges(ctx)
   209  			So(err, ShouldBeNil)
   210  			So(resp, ShouldResembleProto, makeResp(more,
   211  				makeCI(4, 4*time.Millisecond),
   212  				makeCI(1, 1*time.Second),
   213  				makeCI(2, 2*time.Second),
   214  				makeCI(3, 3*time.Second),
   215  			))
   216  			So(FormatTime(now), ShouldEqual, `"2020-02-03 10:30:00.000000000"`)
   217  			So(fake.calls, ShouldResembleProto, []*gerritpb.ListChangesRequest{
   218  				{
   219  					Query: "status:new",
   220  					Limit: 3,
   221  				},
   222  				{
   223  					Query: `status:new before:"2020-02-03 10:29:57.000000000"`, // before:#3.Updated
   224  					Limit: 3,
   225  				},
   226  				{
   227  					Query: `status:new after:"2020-02-03 10:29:59.000000000"`, // after:#1.Updated.
   228  					Limit: 3,
   229  				},
   230  			})
   231  		})
   232  
   233  		Convey("Return partial result on errors", func() {
   234  			pager.opts.Limit = 4
   235  			pager.opts.PageSize = 3
   236  			fake.reset(
   237  				makeResp(more, makeCI(1, 1*time.Second), makeCI(2, 2*time.Second), makeCI(3, 3*time.Second)),
   238  				status.Errorf(codes.Internal, "boooo"),
   239  			)
   240  			resp, err := pager.pagingListChanges(ctx)
   241  			So(err, ShouldErrLike, "boooo")
   242  			So(resp, ShouldResembleProto, makeResp(more,
   243  				makeCI(1, 1*time.Second),
   244  				makeCI(2, 2*time.Second),
   245  				makeCI(3, 3*time.Second),
   246  			))
   247  		})
   248  
   249  		Convey("Bail if ordered by updated DESC assumption doesn't hold", func() {
   250  			pager.opts.Limit = 4
   251  			pager.opts.PageSize = 3
   252  			fake.reset(makeResp(more, makeCI(1, 10*time.Second), makeCI(2, 1*time.Second)))
   253  			_, err := pager.pagingListChanges(ctx)
   254  			So(err, ShouldErrLike, "ListChangesResponse.Changes not ordered by updated timestamp")
   255  		})
   256  
   257  		Convey("Bail if too many changes have the same updated timestamp", func() {
   258  			pager.opts.Limit = 4
   259  			pager.opts.PageSize = 2
   260  			fake.reset(
   261  				makeResp(more, makeCI(1, 1*time.Second), makeCI(2, 9*time.Second)),
   262  				makeResp(more, makeCI(2, 9*time.Second), makeCI(3, 9*time.Second)),
   263  				// Strictly speaking, the exact same RPC can be avoided, but such a situation
   264  				// is rare, so let's not complicate code needlessly.
   265  				makeResp(more, makeCI(2, 9*time.Second), makeCI(3, 9*time.Second)),
   266  			)
   267  			resp, err := pager.pagingListChanges(ctx)
   268  			So(err, ShouldErrLike, `PagingListChanges stuck on query:"status:new before:\"2020-02-03 10:29:51.000000000\""`)
   269  			So(resp, ShouldResembleProto, makeResp(more,
   270  				makeCI(1, 1*time.Second),
   271  				makeCI(2, 9*time.Second),
   272  				makeCI(3, 9*time.Second),
   273  			))
   274  			So(fake.calls, ShouldHaveLength, 3)
   275  		})
   276  
   277  		Convey("Bail if too many changes are concurrently updated", func() {
   278  			pager.opts.Limit = 4
   279  			pager.opts.PageSize = 3
   280  			fake.reset(
   281  				makeResp(more, makeCI(1, 1*time.Second), makeCI(2, 2*time.Second), makeCI(3, 3*time.Second)),
   282  				makeResp(more, makeCI(3, 3*time.Second), makeCI(4, 4*time.Second), makeCI(5, 5*time.Second)),
   283  				// Simulate 6 and 7 being updated after first call.
   284  				makeResp(more, makeCI(6, 6*time.Millisecond), makeCI(7, 7*time.Millisecond)),
   285  			)
   286  			_, err := pager.pagingListChanges(ctx)
   287  			So(err, ShouldErrLike, `PagingListChanges can't keep up with the rate of updates`)
   288  			So(err, ShouldErrLike, `Try increasing PagingListChangesOptions.PageSize`)
   289  			So(fake.calls, ShouldHaveLength, 3)
   290  		})
   291  	})
   292  }
   293  
   294  type fakeListChanges struct {
   295  	results        []any
   296  	calls          []*gerritpb.ListChangesRequest
   297  	expectGrpcOpts []grpc.CallOption
   298  }
   299  
   300  func (f *fakeListChanges) reset(results ...any) {
   301  	f.calls = nil
   302  	f.results = results
   303  }
   304  
   305  func (f *fakeListChanges) ListChanges(ctx context.Context, in *gerritpb.ListChangesRequest, opts ...grpc.CallOption) (*gerritpb.ListChangesResponse, error) {
   306  	logging.Debugf(ctx, "faking ListChanges(%s)", in)
   307  	So(opts, ShouldResemble, f.expectGrpcOpts)
   308  	f.calls = append(f.calls, in)
   309  	if len(f.results) == 0 {
   310  		// Unexpected call. List all of them.
   311  		So(f.calls, ShouldBeNil)
   312  		panic("unreachable")
   313  	}
   314  	r := f.results[0]
   315  	f.results = f.results[1:]
   316  	if v, ok := r.(*gerritpb.ListChangesResponse); ok {
   317  		return v, nil
   318  	}
   319  	if v, ok := r.(error); ok {
   320  		return nil, v
   321  	}
   322  	panic(fmt.Errorf("unrecognized result: %v", r))
   323  }