Commits

JeffDonner committed bee52a5

fix issue 50, separate '-m', mark-only collection

Comments (0)

Files changed (2)

              "Terminate expression with ':' to make the first match match "
              "all subsequent tests (usually file-order). ")
 
+    group._addoption('-m',
+        action="append", dest="markfilter", default=[], metavar="FILTEREXPR",
+        help="only run tests which have marks that match given"
+             "mark expression. "
+             "An expression consists of space-separated terms. "
+             "Each term must match. Precede a term with '-' to negate.")
+
 def pytest_collection_modifyitems(items, config):
     keywordexpr = config.option.keyword
-    if not keywordexpr:
+    markfilter = config.option.markfilter
+    if not keywordexpr and not markfilter:
         return
-    selectuntil = False
-    if keywordexpr[-1] == ":":
-        selectuntil = True
-        keywordexpr = keywordexpr[:-1]
 
     remaining = []
     deselected = []
-    for colitem in items:
-        if keywordexpr and skipbykeyword(colitem, keywordexpr):
-            deselected.append(colitem)
-        else:
-            remaining.append(colitem)
-            if selectuntil:
-                keywordexpr = None
+    if keywordexpr:
+        selectuntil = False
+        if keywordexpr[-1] == ":":
+            selectuntil = True
+            keywordexpr = keywordexpr[:-1]
+
+        for colitem in items:
+            if keywordexpr and skipbykeyword(colitem, keywordexpr):
+                deselected.append(colitem)
+            else:
+                remaining.append(colitem)
+                if selectuntil:
+                    keywordexpr = None
+
+    if not keywordexpr:
+        remaining = items
+
+    deselected_by_mark = []
+    # The way this is arranged, marks get to operate only on what
+    # keywords have passed (if no kwds => everything)
+    filter_by_marks(remaining, deselected_by_mark, markfilter)
+    deselected.extend(deselected_by_mark)
 
     if deselected:
         config.hook.pytest_deselected(items=deselected)
     """
     if not keywordexpr:
         return
-    
+
     itemkeywords = getkeywords(colitem)
     for key in filter(None, keywordexpr.split()):
         eor = key[:1] == '-'
          @py.test.mark.slowtest
          def test_function():
             pass
-  
+
     will set a 'slowtest' :class:`MarkInfo` object
     on the ``test_function`` object. """
 
                     marker(func)
         node = node.parent
     item.keywords.update(py.builtin._getfuncdict(func))
+
+
+def filter_by_marks(items, un_items, filterlist):
+    """We modify <items> in-place."""
+    if not filterlist:
+        return
+
+    remaining = set([])
+    deselected = set([])
+    # -m -prod -m dev => filterlist == ['-prod', 'dev']
+    #   -- which means, give me everything that's not prod, and, everything that's dev
+    # -m "-prod dev" => filterlist == ['-prod dev']
+    #   -- which means, give me everything that's both 'not prod, and dev'
+
+    for i, item in enumerate(items):
+        item_marks = get_marks(item)
+        for and_expr in filterlist:
+            is_compound = and_expr.find(' ') >= 0
+            if is_compound:
+                # In this design, 'and' expressions can only ever add to items
+                if and_expr_satisfied(item_marks, and_expr):
+                    print "keeping", "and_expr:", and_expr, "item marks:", item_marks
+                    # keep (or restore after being previously skipped;
+                    # eg prod in "-prod dev prod"
+                    remaining.add(i)
+                    # was it deselected? this latest-so-far appearance takes precedence.
+                    deselected.discard(i)
+                elif i in remaining:
+                    # some other filter passed it - leave it alone
+                    pass
+                else:
+                    deselected.add(i)
+            # We will
+            else:
+                mark = and_expr
+                # single item; because the overall meaning is 'or', we
+                # do not remove earlier items that have been added to
+                # <remainder>.
+                is_neg = mark[:1] == '-'
+                if is_neg:
+                    bare_mark = mark[1:]
+                else:
+                    bare_mark = mark
+
+                # because of our overarching 'or', if the mark is absent we
+                # have no opinion on it
+                if mark_present(bare_mark, item_marks):
+                    if is_neg:
+                        remaining.discard(i)
+                        deselected.add(i)
+                    else:
+                        remaining.add(i)
+                        deselected.discard(i)
+
+        # given that the top-level filters are 'or', finally, if none
+        # has selected them, we must remove the item (given that, by being
+        # here, we know that /some/ filter criteria has/have been given).
+        if i not in remaining:
+            deselected.add(i)
+
+    if deselected:
+        un_items[:]=[items[i] for i in sorted(deselected)]
+        items[:] = [items[i] for i in sorted(remaining)]
+
+# blatantly lifted from _pytest.mark.py
+def and_expr_satisfied(item_marks, and_expr):
+    if not and_expr:
+        # leave any keyword-selected items as they were
+        return False
+
+    for mark in and_expr.split():
+        is_neg = mark[:1] == '-'
+        if is_neg:
+            mark = mark[1:]
+
+        # '-mark' and not present, or, 'mark' and present are ok
+        if is_neg == mark_present(mark, item_marks):
+            # any mismatch => the 'and' fails.
+            return False
+    # didn't have any mismatches - it's ok by us
+    return True
+
+def single_says_skip(item_marks, mark):
+    if not single_expr: return False
+
+    is_neg = mark[:1] == '-'
+    if is_neg:
+        mark = mark[1:]
+    return is_neg == mark_present(mark, item_marks)
+
+# blatantly lifted from _pytest.mark.py but checks now for whether it is
+# a MarkInfo object
+def get_marks(node):
+    marks = {}
+    while node is not None:
+        for keyword in node.keywords:
+            if isinstance(node.keywords[keyword], MarkInfo):
+                marks[keyword] = node.keywords[keyword]
+        node = node.parent
+    return marks
+
+
+# blatantly lifted from _pytest.mark.py
+# &&& whether there's any intersection between flattened(item_markwords)
+#   and mark.split('.')
+def mark_present(mark, item_markwords):
+    # &&& these are probably foo.bar marks from the @pytest.foo.bar
+    # ... do we have 'dotted' marks? jgd's experiments suggest not.
+    for elem in mark.split("."):
+        # really, want 'flatten' of markwords
+        for mw in item_markwords:
+            if elem in mw:
+                break
+        else:
+            return False
+    return True
+
+
+class MarkFiltration(object):
+    pass

testing/test_collection.py

     ])
 
 
+class TestMarksOnly:
+    def _simple_testfile(self, testdir):
+        if not hasattr(self, 'p'):
+            self.p = testdir.makepyfile("""
+            import pytest
 
+            @pytest.mark.bar
+            def test_foo():
+                pass
+
+            @pytest.mark.foo
+            def test_bar():
+                pass
+
+            @pytest.mark.foo
+            @pytest.mark.bar
+            def test_baz():
+                pass
+        """)
+        return self.p
+
+    def test_check_collect_foo(self, testdir):
+        p = self._simple_testfile(testdir)
+        print "p = ", type(p), self.p
+        reprec = testdir.inline_run("-s", "-m", "foo", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 2
+        acceptable = ['test_baz', 'test_bar']
+        for passd in passed:
+            assert passd.nodeid.split('::')[1] in acceptable
+
+    def test_check_collect_bar(self, testdir):
+        p = self._simple_testfile(testdir)
+        reprec = testdir.inline_run("-s", "-m", "bar", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 2
+        acceptable = ['test_baz', 'test_foo']
+        for passd in passed:
+            assert passd.nodeid.split('::')[1] in acceptable
+
+    def test_check_collect_foo_and_bar(self, testdir):
+        p = self._simple_testfile(testdir)
+        reprec = testdir.inline_run("-s", '-m', "foo bar", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 1
+        assert passed[0].nodeid.split('::')[1] == 'test_baz'
+
+    def test_check_collect_foo_or_bar(self, testdir):
+        p = self._simple_testfile(testdir)
+        reprec = testdir.inline_run("-s", '-m', "foo", "-m", "bar", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 3
+        acceptable = ['test_baz', 'test_foo', 'test_bar']
+        for passd in passed:
+            assert passd.nodeid.split('::')[1] in acceptable
+
+    def test_check_collect_foo_but_not_bar(self, testdir):
+        p = self._simple_testfile(testdir)
+        reprec = testdir.inline_run("-s", '-m', "foo -bar", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 1
+        assert passed[0].nodeid.split('::')[1] == 'test_bar'
+
+    def test_check_collect_foo_but_not_bar_as_or(self, testdir):
+        p = self._simple_testfile(testdir)
+        reprec = testdir.inline_run("-s", '-m', "foo", "-m", "-bar", p)
+        passed, skipped, failed = reprec.listoutcomes()
+        assert len(passed) == 1
+        assert passed[0].nodeid.split('::')[1] == 'test_bar'