1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
14 "git.arvados.org/arvados.git/sdk/go/arvados"
15 "git.arvados.org/arvados.git/sdk/go/arvadostest"
16 check "gopkg.in/check.v1"
19 var _ = check.Suite(&CollectionListSuite{})
21 type collectionLister struct {
23 ItemsToReturn []arvados.Collection
27 func (cl *collectionLister) matchFilters(c arvados.Collection, filters []arvados.Filter) bool {
29 for _, f := range filters {
30 if f.Attr == "uuid" && f.Operator == "=" {
31 s, ok := f.Operand.(string)
32 if ok && s == c.UUID {
35 } else if f.Attr == "uuid" && f.Operator == "in" {
36 if operand, ok := f.Operand.([]string); ok {
37 for _, s := range operand {
42 } else if operand, ok := f.Operand.([]interface{}); ok {
43 for _, s := range operand {
44 if s, ok := s.(string); ok && s == c.UUID {
55 func (cl *collectionLister) CollectionList(ctx context.Context, options arvados.ListOptions) (resp arvados.CollectionList, _ error) {
56 cl.APIStub.CollectionList(ctx, options)
57 for _, c := range cl.ItemsToReturn {
58 if cl.MaxPageSize > 0 && len(resp.Items) >= cl.MaxPageSize {
61 if options.Limit >= 0 && len(resp.Items) >= options.Limit {
64 if cl.matchFilters(c, options.Filters) {
65 if reflect.DeepEqual(options.Select, []string{"uuid", "name"}) {
66 c = arvados.Collection{UUID: c.UUID, Name: c.Name}
67 } else if reflect.DeepEqual(options.Select, []string{"name"}) {
68 c = arvados.Collection{Name: c.Name}
69 } else if len(options.Select) > 0 {
70 panic(fmt.Sprintf("not implemented: options=%#v", options))
72 resp.Items = append(resp.Items, c)
78 type CollectionListSuite struct {
80 ids []string // aaaaa, bbbbb, ccccc
81 uuids [][]string // [[aa-*, aa-*, aa-*], [bb-*, bb-*, ...], ...]
82 backends []*collectionLister
85 func (s *CollectionListSuite) SetUpTest(c *check.C) {
86 s.FederationSuite.SetUpTest(c)
91 for i, id := range []string{"aaaaa", "bbbbb", "ccccc"} {
92 cl := &collectionLister{}
93 s.ids = append(s.ids, id)
94 s.uuids = append(s.uuids, nil)
95 for j := 0; j < 5; j++ {
96 uuid := fmt.Sprintf("%s-4zz18-%s%010d", id, id, j)
97 s.uuids[i] = append(s.uuids[i], uuid)
98 cl.ItemsToReturn = append(cl.ItemsToReturn, arvados.Collection{
102 s.backends = append(s.backends, cl)
106 // call some backends directly via API
107 s.addDirectRemote(c, id, cl)
109 // call some backends through rpc->router->API
110 // to ensure nothing is lost in translation
111 s.addHTTPRemote(c, id, cl)
116 type listTrial struct {
121 filters []arvados.Filter
122 selectfields []string
124 expectCalls []int // number of API calls to backends
128 func (s *CollectionListSuite) TestCollectionListNoUUIDFilters(c *check.C) {
132 expectUUIDs: []string{s.uuids[0][0]},
133 expectCalls: []int{1, 0, 0},
137 func (s *CollectionListSuite) TestCollectionListOneLocal(c *check.C) {
141 filters: []arvados.Filter{{"uuid", "=", s.uuids[0][0]}},
142 expectUUIDs: []string{s.uuids[0][0]},
143 expectCalls: []int{1, 0, 0},
147 func (s *CollectionListSuite) TestCollectionListOneRemote(c *check.C) {
151 filters: []arvados.Filter{{"uuid", "=", s.uuids[1][0]}},
152 expectUUIDs: []string{s.uuids[1][0]},
153 expectCalls: []int{0, 1, 0},
157 func (s *CollectionListSuite) TestCollectionListOneLocalDeselectingUUID(c *check.C) {
161 filters: []arvados.Filter{{"uuid", "=", s.uuids[0][0]}},
162 selectfields: []string{"name"},
163 expectUUIDs: []string{""}, // select=name is honored
164 expectCalls: []int{1, 0, 0},
168 func (s *CollectionListSuite) TestCollectionListOneLocalUsingInOperator(c *check.C) {
172 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0]}}},
173 expectUUIDs: []string{s.uuids[0][0]},
174 expectCalls: []int{1, 0, 0},
178 func (s *CollectionListSuite) TestCollectionListOneRemoteUsingInOperator(c *check.C) {
182 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[1][1]}}},
183 expectUUIDs: []string{s.uuids[1][1]},
184 expectCalls: []int{0, 1, 0},
188 func (s *CollectionListSuite) TestCollectionListOneRemoteDeselectingUUID(c *check.C) {
192 filters: []arvados.Filter{{"uuid", "=", s.uuids[1][0]}},
193 selectfields: []string{"name"},
194 expectUUIDs: []string{s.uuids[1][0]}, // uuid is returned, despite not being selected
195 expectCalls: []int{0, 1, 0},
199 func (s *CollectionListSuite) TestCollectionListOneLocalOneRemote(c *check.C) {
203 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}}},
204 expectUUIDs: []string{s.uuids[0][0], s.uuids[1][0]},
205 expectCalls: []int{1, 1, 0},
209 func (s *CollectionListSuite) TestCollectionListOneLocalOneRemoteDeselectingUUID(c *check.C) {
213 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}}},
214 selectfields: []string{"name"},
215 expectUUIDs: []string{s.uuids[0][0], s.uuids[1][0]}, // uuid is returned, despite not being selected
216 expectCalls: []int{1, 1, 0},
220 func (s *CollectionListSuite) TestCollectionListTwoRemotes(c *check.C) {
224 filters: []arvados.Filter{{"uuid", "in", []string{s.uuids[2][0], s.uuids[1][0]}}},
225 expectUUIDs: []string{s.uuids[1][0], s.uuids[2][0]},
226 expectCalls: []int{0, 1, 1},
230 func (s *CollectionListSuite) TestCollectionListSatisfyAllFilters(c *check.C) {
231 s.cluster.API.MaxItemsPerResponse = 2
235 filters: []arvados.Filter{
236 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][1], s.uuids[2][0], s.uuids[2][1], s.uuids[2][2]}},
237 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][2], s.uuids[2][1]}},
239 expectUUIDs: []string{s.uuids[0][0], s.uuids[2][1]},
240 expectCalls: []int{1, 0, 1},
244 func (s *CollectionListSuite) TestCollectionListEmptySet(c *check.C) {
248 filters: []arvados.Filter{{"uuid", "in", []string{}}},
249 expectUUIDs: []string{},
250 expectCalls: []int{0, 0, 0},
254 func (s *CollectionListSuite) TestCollectionListUnmatchableUUID(c *check.C) {
258 filters: []arvados.Filter{
259 {"uuid", "in", []string{s.uuids[0][0], "abcdefg"}},
260 {"uuid", "in", []string{s.uuids[0][0], "bbbbb-4zz18-bogus"}},
261 {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-bogus"}},
263 expectUUIDs: []string{s.uuids[0][0]},
264 expectCalls: []int{1, 0, 0},
268 func (s *CollectionListSuite) TestCollectionListMultiPage(c *check.C) {
269 for i := range s.backends {
270 s.uuids[i] = s.uuids[i][:3]
271 s.backends[i].ItemsToReturn = s.backends[i].ItemsToReturn[:3]
273 s.cluster.API.MaxItemsPerResponse = 9
274 for _, stub := range s.backends {
277 allUUIDs := append(append(append([]string(nil), s.uuids[0]...), s.uuids[1]...), s.uuids[2]...)
281 filters: []arvados.Filter{{"uuid", "in", append([]string(nil), allUUIDs...)}},
282 expectUUIDs: allUUIDs,
283 expectCalls: []int{2, 2, 2},
287 func (s *CollectionListSuite) TestCollectionListMultiSiteExtraFilters(c *check.C) {
288 // not [yet] supported
292 filters: []arvados.Filter{
293 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
294 {"uuid", "is_a", "teapot"},
296 expectCalls: []int{0, 0, 0},
297 expectStatus: http.StatusBadRequest,
301 func (s *CollectionListSuite) TestCollectionListMultiSiteWithCount(c *check.C) {
302 for _, count := range []string{"", "exact"} {
306 filters: []arvados.Filter{
307 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
308 {"uuid", "is_a", "teapot"},
310 expectCalls: []int{0, 0, 0},
311 expectStatus: http.StatusBadRequest,
316 func (s *CollectionListSuite) TestCollectionListMultiSiteWithLimit(c *check.C) {
317 for _, limit := range []int{0, 1, 2} {
321 filters: []arvados.Filter{
322 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
323 {"uuid", "is_a", "teapot"},
325 expectCalls: []int{0, 0, 0},
326 expectStatus: http.StatusBadRequest,
331 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOffset(c *check.C) {
336 filters: []arvados.Filter{
337 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
338 {"uuid", "is_a", "teapot"},
340 expectCalls: []int{0, 0, 0},
341 expectStatus: http.StatusBadRequest,
345 func (s *CollectionListSuite) TestCollectionListMultiSiteWithOrder(c *check.C) {
349 order: []string{"uuid desc"},
350 filters: []arvados.Filter{
351 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
352 {"uuid", "is_a", "teapot"},
354 expectCalls: []int{0, 0, 0},
355 expectStatus: http.StatusBadRequest,
359 func (s *CollectionListSuite) TestCollectionListInvalidFilters(c *check.C) {
363 filters: []arvados.Filter{
364 {"uuid", "in", "teapot"},
366 expectCalls: []int{0, 0, 0},
367 expectStatus: http.StatusBadRequest,
371 func (s *CollectionListSuite) TestCollectionListRemoteUnknown(c *check.C) {
375 filters: []arvados.Filter{
376 {"uuid", "in", []string{s.uuids[0][0], "bogus-4zz18-000001111122222"}},
378 expectStatus: http.StatusNotFound,
382 func (s *CollectionListSuite) TestCollectionListRemoteError(c *check.C) {
383 s.addDirectRemote(c, "bbbbb", &arvadostest.APIStub{Error: fmt.Errorf("stub backend error")})
387 filters: []arvados.Filter{
388 {"uuid", "in", []string{s.uuids[0][0], s.uuids[1][0]}},
390 expectStatus: http.StatusBadGateway,
394 func (s *CollectionListSuite) test(c *check.C, trial listTrial) {
395 resp, err := s.fed.CollectionList(s.ctx, arvados.ListOptions{
398 Offset: trial.offset,
400 Filters: trial.filters,
401 Select: trial.selectfields,
403 if trial.expectStatus != 0 {
404 c.Assert(err, check.NotNil)
405 err, ok := err.(interface{ HTTPStatus() int })
406 c.Assert(ok, check.Equals, true) // err must implement interface{ HTTPStatus() int }
407 c.Check(err.HTTPStatus(), check.Equals, trial.expectStatus)
408 c.Logf("returned error is %#v", err)
409 c.Logf("returned error string is %q", err)
411 c.Check(err, check.IsNil)
412 expectItems := []arvados.Collection{}
413 for _, uuid := range trial.expectUUIDs {
414 expectItems = append(expectItems, arvados.Collection{UUID: uuid})
416 // expectItems is sorted by UUID, so sort resp.Items
417 // by UUID before checking DeepEquals.
418 sort.Slice(resp.Items, func(i, j int) bool { return resp.Items[i].UUID < resp.Items[j].UUID })
419 c.Check(resp, check.DeepEquals, arvados.CollectionList{
424 for i, stub := range s.backends {
425 if i >= len(trial.expectCalls) {
428 calls := stub.Calls(nil)
429 c.Check(calls, check.HasLen, trial.expectCalls[i])
433 opts := calls[0].Options.(arvados.ListOptions)
434 c.Check(opts.Limit, check.Equals, trial.limit)