| // 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.MaxInt64 | |
| } | |
| // 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 | |
| } |