Repository: buildbot/buildbot

Issue:
REST API: way to query lists for "tag1" or "tag2" (as opposed to and)
The query `/api/v2/builders?tags__contains=tag1&tags__contains=tag2` returns only builders that have *both* tags `tag1` and `tag2`. I don't see a way to query for builders that have either `tag1` or `tag2`.


Discussion:

Resolving patch:
diff --git a/master/buildbot/data/resultspec.py b/master/buildbot/data/resultspec.py
--- a/master/buildbot/data/resultspec.py
+++ b/master/buildbot/data/resultspec.py
@@ -46,7 +46,7 @@ class FieldBase(object):
     plural_operators = {
         'eq': lambda d, v: d in v,
         'ne': lambda d, v: d not in v,
-        'contains': lambda d, v: set(v) <= set(d),
+        'contains': lambda d, v: len(set(v).intersection(set(d))) > 0,
     }
 
     def __init__(self, field, op, values):


Test patch:
diff --git a/master/buildbot/test/unit/test_data_builders.py b/master/buildbot/test/unit/test_data_builders.py
--- a/master/buildbot/test/unit/test_data_builders.py
+++ b/master/buildbot/test/unit/test_data_builders.py
@@ -22,6 +22,7 @@
 from twisted.trial import unittest
 
 from buildbot.data import builders
+from buildbot.data import resultspec
 from buildbot.test.fake import fakedb
 from buildbot.test.fake import fakemaster
 from buildbot.test.util import endpoint
@@ -106,6 +107,15 @@ def setUp(self):
         return self.db.insertTestData([
             fakedb.Builder(id=1, name=u'buildera'),
             fakedb.Builder(id=2, name=u'builderb'),
+            fakedb.Builder(id=3, name=u'builderTagA'),
+            fakedb.Builder(id=4, name=u'builderTagB'),
+            fakedb.Builder(id=5, name=u'builderTagAB'),
+            fakedb.Tag(id=3, name=u"tagA"),
+            fakedb.Tag(id=4, name=u"tagB"),
+            fakedb.BuildersTags(builderid=3, tagid=3),
+            fakedb.BuildersTags(builderid=4, tagid=4),
+            fakedb.BuildersTags(builderid=5, tagid=3),
+            fakedb.BuildersTags(builderid=5, tagid=4),
             fakedb.Master(id=13),
             fakedb.BuilderMaster(id=1, builderid=2, masterid=13),
         ])
@@ -120,7 +130,7 @@ def test_get(self):
         def check(builders):
             [self.validateData(b) for b in builders]
             self.assertEqual(sorted([b['builderid'] for b in builders]),
-                             [1, 2])
+                             [1, 2, 3, 4, 5])
         return d
 
     def test_get_masterid(self):
@@ -142,6 +152,45 @@ def check(builders):
                              [])
         return d
 
+    def test_get_contains_one_tag(self):
+        resultSpec = resultspec.ResultSpec(
+            filters=[resultspec.Filter('tags', 'contains', ["tagA"])])
+        d = self.callGet(('builders',))
+
+        @d.addCallback
+        def check(builders):
+            builders = resultSpec.apply(builders)
+            [self.validateData(b) for b in builders]
+            self.assertEqual(sorted([b['builderid'] for b in builders]),
+                             [3, 5])
+        return d
+
+    def test_get_contains_two_tags(self):
+        resultSpec = resultspec.ResultSpec(
+            filters=[resultspec.Filter('tags', 'contains', ["tagA", "tagB"])])
+        d = self.callGet(('builders',))
+
+        @d.addCallback
+        def check(builders):
+            builders = resultSpec.apply(builders)
+            [self.validateData(b) for b in builders]
+            self.assertEqual(sorted([b['builderid'] for b in builders]),
+                             [3, 4, 5])
+        return d
+
+    def test_get_contains_two_tags_one_unknown(self):
+        resultSpec = resultspec.ResultSpec(
+            filters=[resultspec.Filter('tags', 'contains', ["tagA", "tagC"])])
+        d = self.callGet(('builders',))
+
+        @d.addCallback
+        def check(builders):
+            builders = resultSpec.apply(builders)
+            [self.validateData(b) for b in builders]
+            self.assertEqual(sorted([b['builderid'] for b in builders]),
+                             [3, 5])
+        return d
+
 
 class Builder(interfaces.InterfaceTests, unittest.TestCase):
 
diff --git a/master/buildbot/test/unit/test_data_resultspec.py b/master/buildbot/test/unit/test_data_resultspec.py
--- a/master/buildbot/test/unit/test_data_resultspec.py
+++ b/master/buildbot/test/unit/test_data_resultspec.py
@@ -76,6 +76,16 @@ def test_ge(self):
         self.assertEqual(list(f.apply(mklist('num', 5, 10, 15))),
                          mklist('num', 10, 15))
 
+    def test_contains(self):
+        f = resultspec.Filter('num', 'contains', [10])
+        self.assertEqual(list(f.apply(mklist('num', [5, 1], [10, 1], [15, 1]))),
+                         mklist('num', [10, 1]))
+
+    def test_contains_plural(self):
+        f = resultspec.Filter('num', 'contains', [10, 5])
+        self.assertEqual(list(f.apply(mklist('num', [5, 1], [10, 1], [15, 1]))),
+                         mklist('num', [5, 1], [10, 1]))
+
 
 class ResultSpec(unittest.TestCase):