k8s.io/client-go@v0.22.2/tools/pager/pager_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package pager 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "testing" 24 "time" 25 26 "k8s.io/apimachinery/pkg/api/errors" 27 metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 30 "k8s.io/apimachinery/pkg/runtime" 31 ) 32 33 func list(count int, rv string) *metainternalversion.List { 34 var list metainternalversion.List 35 for i := 0; i < count; i++ { 36 list.Items = append(list.Items, &metav1beta1.PartialObjectMetadata{ 37 ObjectMeta: metav1.ObjectMeta{ 38 Name: fmt.Sprintf("%d", i), 39 }, 40 }) 41 } 42 list.ResourceVersion = rv 43 return &list 44 } 45 46 type testPager struct { 47 t *testing.T 48 rv string 49 index int 50 remaining int 51 last int 52 continuing bool 53 done bool 54 expectPage int64 55 } 56 57 func (p *testPager) reset() { 58 p.continuing = false 59 p.remaining += p.index 60 p.index = 0 61 p.last = 0 62 p.done = false 63 } 64 65 func (p *testPager) PagedList(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { 66 if p.done { 67 p.t.Errorf("did not expect additional call to paged list") 68 return nil, fmt.Errorf("unexpected list call") 69 } 70 expectedContinue := fmt.Sprintf("%s:%d", p.rv, p.last) 71 if options.Limit != p.expectPage || (p.continuing && options.Continue != expectedContinue) { 72 p.t.Errorf("invariant violated, expected limit %d and continue %s, got %#v", p.expectPage, expectedContinue, options) 73 return nil, fmt.Errorf("invariant violated") 74 } 75 if options.Continue != "" && options.ResourceVersion != "" { 76 p.t.Errorf("invariant violated, specifying resource version (%s) is not allowed when using continue (%s).", options.ResourceVersion, options.Continue) 77 return nil, fmt.Errorf("invariant violated") 78 } 79 var list metainternalversion.List 80 total := options.Limit 81 if total == 0 { 82 total = int64(p.remaining) 83 } 84 for i := int64(0); i < total; i++ { 85 if p.remaining <= 0 { 86 break 87 } 88 list.Items = append(list.Items, &metav1beta1.PartialObjectMetadata{ 89 ObjectMeta: metav1.ObjectMeta{ 90 Name: fmt.Sprintf("%d", p.index), 91 }, 92 }) 93 p.remaining-- 94 p.index++ 95 } 96 p.last = p.index 97 if p.remaining > 0 { 98 list.Continue = fmt.Sprintf("%s:%d", p.rv, p.last) 99 p.continuing = true 100 } else { 101 p.done = true 102 } 103 list.ResourceVersion = p.rv 104 return &list, nil 105 } 106 107 func (p *testPager) ExpiresOnSecondPage(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { 108 if p.continuing { 109 p.done = true 110 return nil, errors.NewResourceExpired("this list has expired") 111 } 112 return p.PagedList(ctx, options) 113 } 114 115 func (p *testPager) ExpiresOnSecondPageThenFullList(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { 116 if p.continuing { 117 p.reset() 118 p.expectPage = 0 119 return nil, errors.NewResourceExpired("this list has expired") 120 } 121 return p.PagedList(ctx, options) 122 } 123 124 func TestListPager_List(t *testing.T) { 125 type fields struct { 126 PageSize int64 127 PageFn ListPageFunc 128 FullListIfExpired bool 129 } 130 type args struct { 131 ctx context.Context 132 options metav1.ListOptions 133 } 134 tests := []struct { 135 name string 136 fields fields 137 args args 138 want runtime.Object 139 wantPaged bool 140 wantErr bool 141 isExpired bool 142 }{ 143 { 144 name: "empty page", 145 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 0, rv: "rv:20"}).PagedList}, 146 args: args{}, 147 want: list(0, "rv:20"), 148 wantPaged: false, 149 }, 150 { 151 name: "one page", 152 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 9, rv: "rv:20"}).PagedList}, 153 args: args{}, 154 want: list(9, "rv:20"), 155 wantPaged: false, 156 }, 157 { 158 name: "one full page", 159 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 10, rv: "rv:20"}).PagedList}, 160 args: args{}, 161 want: list(10, "rv:20"), 162 wantPaged: false, 163 }, 164 { 165 name: "two pages", 166 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 11, rv: "rv:20"}).PagedList}, 167 args: args{}, 168 want: list(11, "rv:20"), 169 wantPaged: true, 170 }, 171 { 172 name: "three pages", 173 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 21, rv: "rv:20"}).PagedList}, 174 args: args{}, 175 want: list(21, "rv:20"), 176 wantPaged: true, 177 }, 178 { 179 name: "expires on second page", 180 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 21, rv: "rv:20"}).ExpiresOnSecondPage}, 181 args: args{}, 182 wantPaged: true, 183 wantErr: true, 184 isExpired: true, 185 }, 186 { 187 name: "expires on second page and then lists", 188 fields: fields{ 189 FullListIfExpired: true, 190 PageSize: 10, 191 PageFn: (&testPager{t: t, expectPage: 10, remaining: 21, rv: "rv:20"}).ExpiresOnSecondPageThenFullList, 192 }, 193 args: args{}, 194 want: list(21, "rv:20"), 195 wantPaged: true, 196 }, 197 { 198 name: "two pages with resourceVersion", 199 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 11, rv: "rv:20"}).PagedList}, 200 args: args{options: metav1.ListOptions{ResourceVersion: "rv:10"}}, 201 want: list(11, "rv:20"), 202 wantPaged: true, 203 }, 204 } 205 for _, tt := range tests { 206 t.Run(tt.name, func(t *testing.T) { 207 p := &ListPager{ 208 PageSize: tt.fields.PageSize, 209 PageFn: tt.fields.PageFn, 210 FullListIfExpired: tt.fields.FullListIfExpired, 211 } 212 ctx := tt.args.ctx 213 if ctx == nil { 214 ctx = context.Background() 215 } 216 got, paginatedResult, err := p.List(ctx, tt.args.options) 217 if (err != nil) != tt.wantErr { 218 t.Errorf("ListPager.List() error = %v, wantErr %v", err, tt.wantErr) 219 return 220 } 221 if tt.isExpired != errors.IsResourceExpired(err) { 222 t.Errorf("ListPager.List() error = %v, isExpired %v", err, tt.isExpired) 223 return 224 } 225 if tt.wantPaged != paginatedResult { 226 t.Errorf("paginatedResult = %t, want %t", paginatedResult, tt.wantPaged) 227 } 228 if !reflect.DeepEqual(got, tt.want) { 229 t.Errorf("ListPager.List() = %v, want %v", got, tt.want) 230 } 231 }) 232 } 233 } 234 235 func TestListPager_EachListItem(t *testing.T) { 236 type fields struct { 237 PageSize int64 238 PageFn ListPageFunc 239 } 240 tests := []struct { 241 name string 242 fields fields 243 want runtime.Object 244 wantErr bool 245 wantPanic bool 246 isExpired bool 247 processorErrorOnItem int 248 processorPanicOnItem int 249 cancelContextOnItem int 250 }{ 251 { 252 name: "empty page", 253 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 0, rv: "rv:20"}).PagedList}, 254 want: list(0, "rv:20"), 255 }, 256 { 257 name: "one page", 258 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 9, rv: "rv:20"}).PagedList}, 259 want: list(9, "rv:20"), 260 }, 261 { 262 name: "one full page", 263 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 10, rv: "rv:20"}).PagedList}, 264 want: list(10, "rv:20"), 265 }, 266 { 267 name: "two pages", 268 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 11, rv: "rv:20"}).PagedList}, 269 want: list(11, "rv:20"), 270 }, 271 { 272 name: "three pages", 273 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 21, rv: "rv:20"}).PagedList}, 274 want: list(21, "rv:20"), 275 }, 276 { 277 name: "expires on second page", 278 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 21, rv: "rv:20"}).ExpiresOnSecondPage}, 279 want: list(10, "rv:20"), // all items on the first page should have been visited 280 wantErr: true, 281 isExpired: true, 282 }, 283 { 284 name: "error processing item", 285 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 51, rv: "rv:20"}).PagedList}, 286 want: list(3, "rv:20"), // all the items <= the one the processor returned an error on should have been visited 287 wantPanic: true, 288 processorPanicOnItem: 3, 289 }, 290 { 291 name: "cancel context while processing", 292 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 51, rv: "rv:20"}).PagedList}, 293 want: list(3, "rv:20"), // all the items <= the one the processor returned an error on should have been visited 294 wantErr: true, 295 cancelContextOnItem: 3, 296 }, 297 { 298 name: "panic processing item", 299 fields: fields{PageSize: 10, PageFn: (&testPager{t: t, expectPage: 10, remaining: 51, rv: "rv:20"}).PagedList}, 300 want: list(3, "rv:20"), // all the items <= the one the processor returned an error on should have been visited 301 wantPanic: true, 302 }, 303 } 304 305 processorErr := fmt.Errorf("processor error") 306 for _, tt := range tests { 307 t.Run(tt.name, func(t *testing.T) { 308 ctx, cancel := context.WithCancel(context.Background()) 309 p := &ListPager{ 310 PageSize: tt.fields.PageSize, 311 PageFn: tt.fields.PageFn, 312 } 313 var items []runtime.Object 314 315 fn := func(obj runtime.Object) error { 316 items = append(items, obj) 317 if tt.processorErrorOnItem > 0 && len(items) == tt.processorErrorOnItem { 318 return processorErr 319 } 320 if tt.processorPanicOnItem > 0 && len(items) == tt.processorPanicOnItem { 321 panic(processorErr) 322 } 323 if tt.cancelContextOnItem > 0 && len(items) == tt.cancelContextOnItem { 324 cancel() 325 } 326 return nil 327 } 328 var err error 329 var panic interface{} 330 func() { 331 defer func() { 332 panic = recover() 333 }() 334 err = p.EachListItem(ctx, metav1.ListOptions{}, fn) 335 }() 336 if (panic != nil) && !tt.wantPanic { 337 t.Fatalf(".EachListItem() panic = %v, wantPanic %v", panic, tt.wantPanic) 338 } else { 339 return 340 } 341 if (err != nil) != tt.wantErr { 342 t.Errorf("ListPager.EachListItem() error = %v, wantErr %v", err, tt.wantErr) 343 return 344 } 345 if tt.isExpired != errors.IsResourceExpired(err) { 346 t.Errorf("ListPager.EachListItem() error = %v, isExpired %v", err, tt.isExpired) 347 return 348 } 349 if tt.processorErrorOnItem > 0 && err != processorErr { 350 t.Errorf("ListPager.EachListItem() error = %v, processorErrorOnItem %d", err, tt.processorErrorOnItem) 351 return 352 } 353 l := tt.want.(*metainternalversion.List) 354 if !reflect.DeepEqual(items, l.Items) { 355 t.Errorf("ListPager.EachListItem() = %v, want %v", items, l.Items) 356 } 357 }) 358 } 359 } 360 361 func TestListPager_eachListPageBuffered(t *testing.T) { 362 tests := []struct { 363 name string 364 totalPages int 365 pagesProcessed int 366 wantPageLists int 367 pageBufferSize int32 368 pageSize int 369 }{ 370 { 371 name: "no buffer, one total page", 372 totalPages: 1, 373 pagesProcessed: 1, 374 wantPageLists: 1, 375 pageBufferSize: 0, 376 }, { 377 name: "no buffer, 1/5 pages processed", 378 totalPages: 5, 379 pagesProcessed: 1, 380 wantPageLists: 2, // 1 received for processing, 1 listed 381 pageBufferSize: 0, 382 }, 383 { 384 name: "no buffer, 2/5 pages processed", 385 totalPages: 5, 386 pagesProcessed: 2, 387 wantPageLists: 3, 388 pageBufferSize: 0, 389 }, 390 { 391 name: "no buffer, 5/5 pages processed", 392 totalPages: 5, 393 pagesProcessed: 5, 394 wantPageLists: 5, 395 pageBufferSize: 0, 396 }, 397 { 398 name: "size 1 buffer, 1/5 pages processed", 399 totalPages: 5, 400 pagesProcessed: 1, 401 wantPageLists: 3, 402 pageBufferSize: 1, 403 }, 404 { 405 name: "size 1 buffer, 5/5 pages processed", 406 totalPages: 5, 407 pagesProcessed: 5, 408 wantPageLists: 5, 409 pageBufferSize: 1, 410 }, 411 { 412 name: "size 10 buffer, 1/5 page processed", 413 totalPages: 5, 414 pagesProcessed: 1, 415 wantPageLists: 5, 416 pageBufferSize: 10, // buffer is larger than list 417 }, 418 } 419 processorErr := fmt.Errorf("processor error") 420 pageSize := 10 421 for _, tt := range tests { 422 t.Run(tt.name, func(t *testing.T) { 423 pgr := &testPager{t: t, expectPage: int64(pageSize), remaining: tt.totalPages * pageSize, rv: "rv:20"} 424 pageLists := 0 425 wantedPageListsDone := make(chan struct{}) 426 listFn := func(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { 427 pageLists++ 428 if pageLists == tt.wantPageLists { 429 close(wantedPageListsDone) 430 } 431 return pgr.PagedList(ctx, options) 432 } 433 p := &ListPager{ 434 PageSize: int64(pageSize), 435 PageBufferSize: tt.pageBufferSize, 436 PageFn: listFn, 437 } 438 439 pagesProcessed := 0 440 fn := func(obj runtime.Object) error { 441 pagesProcessed++ 442 if tt.pagesProcessed == pagesProcessed && tt.wantPageLists > 0 { 443 // wait for buffering to catch up 444 select { 445 case <-time.After(time.Second): 446 return fmt.Errorf("Timed out waiting for %d page lists", tt.wantPageLists) 447 case <-wantedPageListsDone: 448 } 449 return processorErr 450 } 451 return nil 452 } 453 err := p.eachListChunkBuffered(context.Background(), metav1.ListOptions{}, fn) 454 if tt.pagesProcessed > 0 && err == processorErr { 455 // expected 456 } else if err != nil { 457 t.Fatal(err) 458 } 459 if tt.wantPageLists > 0 && pageLists != tt.wantPageLists { 460 t.Errorf("expected %d page lists, got %d", tt.wantPageLists, pageLists) 461 } 462 if pagesProcessed != tt.pagesProcessed { 463 t.Errorf("expected %d pages processed, got %d", tt.pagesProcessed, pagesProcessed) 464 } 465 }) 466 } 467 }