-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
iterators.go
186 lines (178 loc) · 7.02 KB
/
iterators.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/*
Copyright 2016 Google Inc. All Rights Reserved.
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 testutil
import (
"fmt"
"reflect"
)
// TestIteratorNext tests the Next method of a standard client iterator (see
// https://github.com/GoogleCloudPlatform/google-cloud-go/wiki/Iterator-Guidelines).
//
// This function assumes that an iterator has already been created, and that
// the underlying sequence to be iterated over already exists. want should be a
// slice that contains the elements of this sequence. It must contain at least
// one element.
//
// done is the special error value that signals that the iterator has provided all the items.
//
// next is a function that returns the result of calling Next on the iterator. It can usually be
// defined as
// func() (interface{}, error) { return iter.Next() }
//
// TestIteratorNext checks that the iterator returns all the elements of want
// in order, followed by (zero, done). It also confirms that subsequent calls
// to next also return (zero, done).
//
// On success, TestIteratorNext returns ("", true). On failure, it returns a
// suitable error message and false.
func TestIteratorNext(want interface{}, done error, next func() (interface{}, error)) (string, bool) {
wVal := reflect.ValueOf(want)
if wVal.Kind() != reflect.Slice {
return "'want' must be a slice", false
}
for i := 0; i < wVal.Len(); i++ {
got, err := next()
if err != nil {
return fmt.Sprintf("#%d: got %v, expected an item", i, err), false
}
w := wVal.Index(i).Interface()
if !reflect.DeepEqual(got, w) {
return fmt.Sprintf("#%d: got %+v, want %+v", i, got, w), false
}
}
// We now should see (<zero value of item type>, done), no matter how many
// additional calls we make.
zero := reflect.Zero(wVal.Type().Elem()).Interface()
for i := 0; i < 3; i++ {
got, err := next()
if err != done {
return fmt.Sprintf("at end: got error %v, want done", err), false
}
// Since err == done, got should be zero.
if got != zero {
return fmt.Sprintf("got %+v with done, want zero %T", got, zero), false
}
}
return "", true
}
// PagingIterator describes the standard client iterator pattern with paging as best as possible in Go.
// See https://github.com/GoogleCloudPlatform/google-cloud-go/wiki/Iterator-Guidelines.
type PagingIterator interface {
SetPageSize(int)
SetPageToken(string)
NextPageToken() string
// NextPage() ([]T, error)
}
// TestIteratorNextPageExact tests the NextPage method of a standard client
// iterator with paging (see PagingIterator).
//
// This function assumes that the underlying sequence to be iterated over
// already exists. want should be a slice that contains the elements of this
// sequence. It must contain at least three elements, in order to test
// non-trivial paging behavior.
//
// done is the special error value that signals that the iterator has provided all the items.
//
// defaultPageSize is the page size to use when the user has not called SetPageSize, or calls
// it with a value <= 0.
//
// newIter should return a new iterator each time it is called.
//
// nextPage should return the result of calling NextPage on the iterator. It can usually be
// defined as
// func(i testutil.PagingIterator) (interface{}, error) { return i.(*<iteratorType>).NextPage() }
//
// TestIteratorNextPageExact checks that the iterator returns all the elements
// of want in order, divided into pages of the exactly the right size. It
// confirms that if the last page is partial, done is returned along with it,
// and in any case, done is returned subsequently along with a zero-length
// slice.
//
// On success, TestIteratorNextPageExact returns ("", true). On failure, it returns a
// suitable error message and false.
func TestIteratorNextPageExact(want interface{}, done error, defaultPageSize int, newIter func() PagingIterator, nextPage func(PagingIterator) (interface{}, error)) (string, bool) {
wVal := reflect.ValueOf(want)
if wVal.Kind() != reflect.Slice {
return "'want' must be a slice", false
}
if wVal.Len() < 3 {
return "need at least 3 values for 'want' to effectively test paging", false
}
const doNotSetPageSize = -999
for _, pageSize := range []int{doNotSetPageSize, -7, 0, 1, 2, wVal.Len(), wVal.Len() + 10} {
adjustedPageSize := int(pageSize)
if pageSize <= 0 {
adjustedPageSize = int(defaultPageSize)
}
// Create the pages we expect to see.
var wantPages []interface{}
for i, j := 0, adjustedPageSize; i < wVal.Len(); i, j = j, j+adjustedPageSize {
if j > wVal.Len() {
j = wVal.Len()
}
wantPages = append(wantPages, wVal.Slice(i, j).Interface())
}
for _, usePageToken := range []bool{false, true} {
it := newIter()
if pageSize != doNotSetPageSize {
it.SetPageSize(pageSize)
}
for i, wantPage := range wantPages {
gotPage, err := nextPage(it)
if err != nil && err != done {
return fmt.Sprintf("usePageToken %v, pageSize %d, #%d: got %v, expected a page",
usePageToken, pageSize, i, err), false
}
if !reflect.DeepEqual(gotPage, wantPage) {
return fmt.Sprintf("usePageToken %v, pageSize %d, #%d:\ngot %v\nwant %+v",
usePageToken, pageSize, i, gotPage, wantPage), false
}
// If the last page is partial, NextPage must return done.
if reflect.ValueOf(gotPage).Len() < adjustedPageSize && err != done {
return fmt.Sprintf("usePageToken %v, pageSize %d, #%d: expected done on partial page, got %v",
usePageToken, pageSize, i, err), false
}
if usePageToken {
// Pretend that we are displaying a paginated listing on the web, and the next
// page may be served by a different process.
// Empty page token implies done, and vice versa.
if (it.NextPageToken() == "") != (err == done) {
return fmt.Sprintf("pageSize %d: next page token = %q and err = %v; expected empty page token iff done",
pageSize, it.NextPageToken(), err), false
}
if err == nil {
token := it.NextPageToken()
it = newIter()
it.SetPageSize(pageSize)
it.SetPageToken(token)
}
}
}
// We now should see (<zero-length or nil slice>, done), no matter how many
// additional calls we make.
for i := 0; i < 3; i++ {
gotPage, err := nextPage(it)
if err != done {
return fmt.Sprintf("usePageToken %v, pageSize %d, at end: got error %v, want done",
usePageToken, pageSize, err), false
}
pVal := reflect.ValueOf(gotPage)
if pVal.Kind() != reflect.Slice || pVal.Len() != 0 {
return fmt.Sprintf("usePageToken %v, pageSize %d, at end: got %+v with done, want zero-length slice",
usePageToken, pageSize, gotPage), false
}
}
}
}
return "", true
}