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 }