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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
|
// Copyright 2016 Google LLC.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package iterator_test
import (
"context"
"encoding/json"
"math"
"reflect"
"testing"
"google.golang.org/api/iterator"
itest "google.golang.org/api/iterator/testing"
)
// Service represents the implementation of a Google API's List method.
// We want to test against a large range of possible valid behaviors.
// All the behaviors this can generate are valid under the spec for
// Google API paging.
type service struct {
// End of the sequence. end-1 is the last value returned.
end int
// Maximum number of items to return in one RPC. Also the default page size.
// If zero, max is unlimited.
max int
// If true, return two empty pages before each RPC that returns items, and
// two zero pages at the end. E.g. if end = 5, max = 2 and the pageSize
// parameter to List is zero, then the number of items returned in
// successive RPCS is:
// 0 0 2 0 0 2 0 0 1 0 0
// Note that this implies that the RPC returning the last items will have a
// non-empty page token.
zeroes bool
}
// List simulates an API List RPC. It returns integers in the range [0, s.end).
func (s *service) List(pageSize int, pageToken string) ([]int, string, error) {
max := s.max
if max == 0 {
max = math.MaxInt32
}
// Never give back any more than s.max.
if pageSize <= 0 || pageSize > max {
pageSize = max
}
state := &listState{}
if pageToken != "" {
if err := json.Unmarshal([]byte(pageToken), state); err != nil {
return nil, "", err
}
}
ints := state.advance(pageSize, s.end, s.zeroes)
if state.Start == s.end && (!s.zeroes || state.NumZeroes == 2) {
pageToken = ""
} else {
bytes, err := json.Marshal(state)
if err != nil {
return nil, "", err
}
pageToken = string(bytes)
}
return ints, pageToken, nil
}
type listState struct {
Start int // where to start this page
NumZeroes int // number of consecutive empty pages before this
}
func (s *listState) advance(pageSize, end int, zeroes bool) []int {
var page []int
if zeroes && s.NumZeroes != 2 {
// Return a zero page.
} else {
for i := s.Start; i < end && len(page) < pageSize; i++ {
page = append(page, i)
}
}
s.Start += len(page)
if len(page) == 0 {
s.NumZeroes++
} else {
s.NumZeroes = 0
}
return page
}
func TestServiceList(t *testing.T) {
for _, test := range []struct {
svc service
pageSize int
want [][]int
}{
{service{end: 0}, 0, [][]int{nil}},
{service{end: 5}, 0, [][]int{{0, 1, 2, 3, 4}}},
{service{end: 5}, 8, [][]int{{0, 1, 2, 3, 4}}},
{service{end: 5}, 2, [][]int{{0, 1}, {2, 3}, {4}}},
{service{end: 5, max: 2}, 0, [][]int{{0, 1}, {2, 3}, {4}}},
{service{end: 5, max: 2}, 1, [][]int{{0}, {1}, {2}, {3}, {4}}},
{service{end: 5, max: 2}, 10, [][]int{{0, 1}, {2, 3}, {4}}},
{service{end: 5, zeroes: true}, 0, [][]int{nil, nil, {0, 1, 2, 3, 4}, nil, nil}},
{service{end: 5, max: 3, zeroes: true}, 0, [][]int{nil, nil, {0, 1, 2}, nil, nil, {3, 4}, nil, nil}},
} {
var got [][]int
token := ""
for {
items, nextToken, err := test.svc.List(test.pageSize, token)
if err != nil {
t.Fatalf("%v, %d: %v", test.svc, test.pageSize, err)
}
got = append(got, items)
if nextToken == "" {
break
}
token = nextToken
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("%v, %d: got %v, want %v", test.svc, test.pageSize, got, test.want)
}
}
}
type Client struct{ s *service }
// ItemIterator is a sample implementation of a standard iterator.
type ItemIterator struct {
pageInfo *iterator.PageInfo
nextFunc func() error
s *service
items []int
}
// PageInfo returns a PageInfo, which supports pagination.
func (it *ItemIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
// Items is a sample implementation of an iterator-creating method.
func (c *Client) Items(ctx context.Context) *ItemIterator {
it := &ItemIterator{s: c.s}
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
it.fetch,
func() int { return len(it.items) },
func() interface{} { b := it.items; it.items = nil; return b })
return it
}
func (it *ItemIterator) fetch(pageSize int, pageToken string) (string, error) {
items, tok, err := it.s.List(pageSize, pageToken)
it.items = append(it.items, items...)
return tok, err
}
func (it *ItemIterator) Next() (int, error) {
if err := it.nextFunc(); err != nil {
return 0, err
}
item := it.items[0]
it.items = it.items[1:]
return item, nil
}
func TestNext(t *testing.T) {
// Test the iterator's Next method with a variety of different service behaviors.
// This is primarily a test of PageInfo.next.
for _, svc := range []service{
{end: 0},
{end: 5},
{end: 5, max: 1},
{end: 5, max: 2},
{end: 5, zeroes: true},
{end: 5, max: 2, zeroes: true},
} {
client := &Client{&svc}
msg, ok := itest.TestIterator(
seq(0, svc.end),
func() interface{} { return client.Items(ctx) },
func(it interface{}) (interface{}, error) { return it.(*ItemIterator).Next() })
if !ok {
t.Errorf("%+v: %s", svc, msg)
}
}
}
// TODO(jba): test setting PageInfo.MaxSize
// TODO(jba): test setting PageInfo.Token
// Verify that, for an iterator that uses PageInfo.next to implement its Next
// method, using Next and NextPage together result in an error.
func TestNextWithNextPage(t *testing.T) {
client := &Client{&service{end: 11}}
var items []int
// Calling Next before NextPage.
it := client.Items(ctx)
it.Next()
_, err := iterator.NewPager(it, 1, "").NextPage(&items)
if err == nil {
t.Error("NextPage after Next: got nil, want error")
}
_, err = it.Next()
if err == nil {
t.Error("Next after NextPage: got nil, want error")
}
// Next between two calls to NextPage.
it = client.Items(ctx)
p := iterator.NewPager(it, 1, "")
p.NextPage(&items)
_, err = it.Next()
if err == nil {
t.Error("Next after NextPage: got nil, want error")
}
_, err = p.NextPage(&items)
if err == nil {
t.Error("second NextPage after Next: got nil, want error")
}
}
// Verify that we turn various potential reflection panics into errors.
func TestNextPageReflectionErrors(t *testing.T) {
client := &Client{&service{end: 1}}
p := iterator.NewPager(client.Items(ctx), 1, "")
// Passing the nil interface value.
_, err := p.NextPage(nil)
if err == nil {
t.Error("nil: got nil, want error")
}
// Passing a non-slice.
_, err = p.NextPage(17)
if err == nil {
t.Error("non-slice: got nil, want error")
}
// Passing a slice of the wrong type.
var bools []bool
_, err = p.NextPage(&bools)
if err == nil {
t.Error("wrong type: got nil, want error")
}
// Using a slice of the right type, but not passing a pointer to it.
var ints []int
_, err = p.NextPage(ints)
if err == nil {
t.Error("not a pointer: got nil, want error")
}
}
// seq returns a slice containing the values in [from, to).
func seq(from, to int) []int {
var r []int
for i := from; i < to; i++ {
r = append(r, i)
}
return r
}
|