// 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 iterator_test import ( "encoding/json" "math" "reflect" "testing" "golang.org/x/net/context" "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 }