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  }