// Copyright 2016 Google LLC // // 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 testing provides support functions for testing iterators conforming // to the standard pattern. // See package google.golang.org/api/iterator and // https://github.com/GoogleCloudPlatform/gcloud-golang/wiki/Iterator-Guidelines. package testing import ( "fmt" "reflect" "google.golang.org/api/iterator" ) // TestIterator tests the Next method of a standard iterator. It assumes that // the underlying sequence to be iterated over already exists. // // The want argument should be a slice that contains the elements of this // sequence. It may be an empty slice, but it must not be the nil interface // value. The elements must be comparable with reflect.DeepEqual. // // The create function should create and return a new iterator. // It will typically look like // func() interface{} { return client.Items(ctx) } // // The next function takes the return value of create and should return the // result of calling Next on the iterator. It can usually be defined as // func(it interface{}) (interface{}, error) { return it.(*ItemIterator).Next() } // // TestIterator 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). // // If the iterator implements the method // PageInfo() *iterator.PageInfo // then exact pagination with iterator.Pager is also tested. Pagination testing // will be more informative if the want slice contains at least three elements. // // On success, TestIterator returns ("", true). On failure, it returns a // suitable error message and false. func TestIterator(want interface{}, create func() interface{}, next func(interface{}) (interface{}, error)) (string, bool) { vWant := reflect.ValueOf(want) if vWant.Kind() != reflect.Slice { return "'want' must be a slice", false } it := create() msg, ok := testNext(vWant, it, next) if !ok { return msg, ok } if _, ok := it.(iterator.Pageable); !ok || vWant.Len() == 0 { return "", true } return testPaging(vWant, create, next) } // Check that the iterator returns vWant, the desired sequence. func testNext(vWant reflect.Value, it interface{}, next func(interface{}) (interface{}, error)) (string, bool) { for i := 0; i < vWant.Len(); i++ { got, err := next(it) if err != nil { return fmt.Sprintf("#%d: got %v, expected an item", i, err), false } w := vWant.Index(i).Interface() if !reflect.DeepEqual(got, w) { return fmt.Sprintf("#%d: got %+v, want %+v", i, got, w), false } } // We now should see (, done), no matter how many // additional calls we make. zero := reflect.Zero(vWant.Type().Elem()).Interface() for i := 0; i < 3; i++ { got, err := next(it) if err != iterator.Done { return fmt.Sprintf("at end: got error %v, want iterator.Done", err), false } // Since err == iterator.Done, got should be zero. if got != zero { return fmt.Sprintf("got %+v with done, want zero %T", got, zero), false } } return "", true } // Test the iterator's behavior when used with iterator.Pager. func testPaging(vWant reflect.Value, create func() interface{}, next func(interface{}) (interface{}, error)) (string, bool) { // Test page sizes that are smaller, equal to, and greater than the length // of the expected sequence. for _, pageSize := range []int{1, 2, vWant.Len(), vWant.Len() + 10} { wantPages := wantedPages(vWant, pageSize) // Test the Pager in two ways. // First, by creating a single Pager and calling NextPage in a loop, // ignoring the page token except for detecting the end of the // iteration. it := create().(iterator.Pageable) pager := iterator.NewPager(it, pageSize, "") msg, ok := testPager(fmt.Sprintf("ignore page token, pageSize %d", pageSize), vWant.Type(), wantPages, func(_ string, pagep interface{}) (string, error) { return pager.NextPage(pagep) }) if !ok { return msg, false } // Second, by creating a new Pager for each page, passing in the page // token from the previous page, as would be done in a web handler. it = create().(iterator.Pageable) msg, ok = testPager(fmt.Sprintf("use page token, pageSize %d", pageSize), vWant.Type(), wantPages, func(pageToken string, pagep interface{}) (string, error) { return iterator.NewPager(it, pageSize, pageToken).NextPage(pagep) }) if !ok { return msg, false } } return "", true } // Create the pages we expect to see. func wantedPages(vWant reflect.Value, pageSize int) []interface{} { var pages []interface{} for i, j := 0, pageSize; i < vWant.Len(); i, j = j, j+pageSize { if j > vWant.Len() { j = vWant.Len() } pages = append(pages, vWant.Slice(i, j).Interface()) } return pages } func testPager(prefix string, sliceType reflect.Type, wantPages []interface{}, nextPage func(pageToken string, pagep interface{}) (string, error)) (string, bool) { tok := "" var err error for i := 0; i < len(wantPages)+1; i++ { vpagep := reflect.New(sliceType) tok, err = nextPage(tok, vpagep.Interface()) if err != nil { return fmt.Sprintf("%s, page #%d: got error %v", prefix, i, err), false } if i == len(wantPages) { // Allow one empty page at the end. if vpagep.Elem().Len() != 0 || tok != "" { return fmt.Sprintf("%s: did not get one empty page at end", prefix), false } break } if msg, ok := compareSlices(vpagep.Elem(), reflect.ValueOf(wantPages[i])); !ok { return fmt.Sprintf("%s, page #%d:\n%s", prefix, i, msg), false } if tok == "" { if i != len(wantPages)-1 { return fmt.Sprintf("%s, page #%d: got empty page token", prefix, i), false } break } } return "", true } // Compare two slices element-by-element. If they are equal, return ("", true). // Otherwise, return a description of the difference and false. func compareSlices(vgot, vwant reflect.Value) (string, bool) { if got, want := vgot.Len(), vwant.Len(); got != want { return fmt.Sprintf("got %d items, want %d", got, want), false } for i := 0; i < vgot.Len(); i++ { if got, want := vgot.Index(i).Interface(), vwant.Index(i).Interface(); !reflect.DeepEqual(got, want) { return fmt.Sprintf("got[%d] = %+v\nwant = %+v", i, got, want), false } } return "", true }