diff --git a/CHANGELOG b/CHANGELOG index cba430e14bf..f5d90360da3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- Allow use of keywords like "-k myfile.py::MyClass::my_test", + as output by the pytest runner. + - fix issue713: JUnit XML reports for doctest failures. Thanks Punyashloka Biswal. diff --git a/_pytest/mark.py b/_pytest/mark.py index 817dc72fe98..96b40079d2b 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,6 +1,6 @@ """ generic mechanism for marking and selecting python functions. """ import py - +import re class MarkerError(Exception): @@ -24,7 +24,8 @@ def pytest_addoption(parser): "contains 'test_method' or 'test_other'. " "Additionally keywords are matched to classes and functions " "containing extra names in their 'extra_keyword_matches' set, " - "as well as functions which have names assigned directly to them." + "as well as functions which have names assigned directly to them. " + "The notation \"MyClass::test_method\" is also recognized as a logical AND." ) group._addoption( @@ -71,6 +72,16 @@ def pytest_collection_modifyitems(items, config): selectuntil = True keywordexpr = keywordexpr[:-1] + # we recognize keywords in the form "[myfile.py::]MyClass::my_test", as + # output by py.test, although we ignore the "myfile.py" part and only consider + # class and function substrings, for now + def _colon_pair_replacer(match_obj): + return "(%s and %s)" % (match_obj.group(2), match_obj.group(3)) + colon_pair_regex = r'(?iu)\b(\S*?::)?(\w+)::(\w+)(\b|\s)' + keywordexpr = re.sub(colon_pair_regex, + _colon_pair_replacer, + keywordexpr) + remaining = [] deselected = [] for colitem in items: diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.txt index f001965aec7..bf074a7d892 100644 --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -163,6 +163,20 @@ Or to select "http" and "quick" tests:: ======= 2 tests deselected by '-khttp or quick' ======== ======= 2 passed, 2 deselected in 0.12 seconds ======== +Colon-pair notation (eg. copied from py.test output) also works, as a logical AND:: + + $ py.test -k "Class::test_meth" -v + ======= test session starts ======== + platform linux2 -- Python 2.7.9, pytest-2.8.0.dev4, py-1.4.28, pluggy-0.3.0 -- $PWD/.env/bin/python2.7 + rootdir: $REGENDOC_TMPDIR, inifile: + collecting ... collected 4 items + + test_server.py::TestClass::test_method PASSED + + ======= 3 tests deselected by '-kClass::test_meth' ======== + ======= 1 passed, 3 deselected in 0.12 seconds ======== + + .. note:: If you are using expressions such as "X and Y" then both X and Y diff --git a/doc/en/usage.txt b/doc/en/usage.txt index e774ebef667..8e8945301f4 100644 --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -46,7 +46,7 @@ Several test run options:: py.test test_mod.py # run tests in module py.test somepath # run all tests below somepath py.test -k stringexpr # only run tests with names that match the - # the "string expression", e.g. "MyClass and not method" + # "string expression", e.g. "MyClass and not method" # will select TestMyClass.test_something # but not TestMyClass.test_method_simple py.test test_mod.py::test_func # only run tests that match the "node ID", diff --git a/testing/test_mark.py b/testing/test_mark.py index eb2e10f3d87..6bd649e00b2 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -4,7 +4,7 @@ class TestMark: def test_markinfo_repr(self): from _pytest.mark import MarkInfo - m = MarkInfo("hello", (1,2), {}) + m = MarkInfo("hello", (1, 2), {}) repr(m) def test_pytest_exists_in_namespace_all(self): @@ -109,7 +109,7 @@ def test_markers_option(testdir): a1: this is a webtest marker a1some: another marker """) - result = testdir.runpytest("--markers", ) + result = testdir.runpytest("--markers",) result.stdout.fnmatch_lines([ "*a1*this is a webtest*", "*a1some*another marker", @@ -214,6 +214,8 @@ def test_nointer(): ("not interface", ("test_nointer", "test_pass")), ("pass", ("test_pass",)), ("not pass", ("test_interface", "test_nointer")), + ("inte and face", ("test_interface",)), + ("inte and r and not face", ("test_nointer",)), ]) def test_keyword_option_custom(spec, testdir): testdir.makepyfile(""" @@ -231,6 +233,33 @@ def test_pass(): assert len(passed) == len(passed_result) assert list(passed) == list(passed_result) +@pytest.mark.parametrize("spec", [ + ("MyClass::test_ab", True, ("test_ab",)), + ("lass::_a", False, ("test_ab",)), + ("lass::b", True, ("test_ab", "test_bc")), + ("badClass::test_ab", False, ()), + ("test_ab", True, ()), # "file.py::function" doesn't work + ("test_ab or MyClass::test_bc", False, ("test_ab", "test_bc")), +]) +def test_keyword_with_colon_pairs(spec, testdir): + localfile = testdir.makepyfile(""" + class TestMyClass: + def test_ab(self): + pass + def test_bc(self): + pass + def test_cd(self): + pass + """) + filename = localfile.basename + opt, add_filename, passed_result = spec + if add_filename: + opt = filename + "::" + opt + rec = testdir.inline_run("-k", opt) + passed, skipped, fail = rec.listoutcomes() + passed = [x.nodeid.split("::")[-1] for x in passed] + assert len(passed) == len(passed_result) + assert list(passed) == list(passed_result) @pytest.mark.parametrize("spec", [ ("None", ("test_func[None]",)), @@ -348,7 +377,7 @@ def test_func(self): assert len(l) == 3 assert l[0].args == ("pos0",) assert l[1].args == () - assert l[2].args == ("pos1", ) + assert l[2].args == ("pos1",) @pytest.mark.xfail(reason='unfixed') def test_merging_markers_deep(self, testdir):