diff --git a/CHANGELOG b/CHANGELOG index 29436e084..c51748f8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,9 +7,10 @@ Changes between 2.1.3 and XXX 2.2.0 allowing to avoid typos and maintain a well described set of markers for your test suite. See exaples at http://pytest.org/latest/mark.html and its links. -- XXX introduce "-m marker" option to select tests based on markers - (this is a stricter more predictable version of '-k' which also matches - substrings and compares against the test function name etc.) +- introduce "-m marker" option to select tests based on markers + (this is a stricter and more predictable version of '-k' in that + "-m" only matches complete markers and has more obvious rules + for and/or semantics. - new feature to help optimizing the speed of your tests: --durations=N option for displaying N slowest test calls and setup/teardown methods. diff --git a/_pytest/__init__.py b/_pytest/__init__.py index c0fe93997..30bd9a19d 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.2.0.dev6' +__version__ = '2.2.0.dev7' diff --git a/_pytest/mark.py b/_pytest/mark.py index 96ad2af6a..953b520ae 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -14,6 +14,12 @@ def pytest_addoption(parser): "Terminate expression with ':' to make the first match match " "all subsequent tests (usually file-order). ") + group._addoption("-m", + action="store", dest="markexpr", default="", metavar="MARKEXPR", + help="only run tests which match given mark expression. " + "An expression is a python expression which can use " + "marker names.") + group.addoption("--markers", action="store_true", help= "show markers (builtin, plugin and per-project ones).") @@ -34,10 +40,11 @@ pytest_cmdline_main.tryfirst = True def pytest_collection_modifyitems(items, config): keywordexpr = config.option.keyword - if not keywordexpr: + matchexpr = config.option.markexpr + if not keywordexpr and not matchexpr: return selectuntil = False - if keywordexpr[-1] == ":": + if keywordexpr[-1:] == ":": selectuntil = True keywordexpr = keywordexpr[:-1] @@ -47,14 +54,27 @@ def pytest_collection_modifyitems(items, config): if keywordexpr and skipbykeyword(colitem, keywordexpr): deselected.append(colitem) else: - remaining.append(colitem) if selectuntil: keywordexpr = None + if matchexpr: + if not matchmark(colitem, matchexpr): + deselected.append(colitem) + continue + remaining.append(colitem) if deselected: config.hook.pytest_deselected(items=deselected) items[:] = remaining +class BoolDict: + def __init__(self, mydict): + self._mydict = mydict + def __getitem__(self, name): + return name in self._mydict + +def matchmark(colitem, matchexpr): + return eval(matchexpr, {}, BoolDict(colitem.obj.__dict__)) + def pytest_configure(config): if config.option.strict: pytest.mark._config = config diff --git a/_pytest/terminal.py b/_pytest/terminal.py index c8c399312..76b91b138 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -440,8 +440,15 @@ class TerminalReporter: def summary_deselected(self): if 'deselected' in self.stats: + l = [] + k = self.config.option.keyword + if k: + l.append("-k%s" % k) + m = self.config.option.markexpr + if m: + l.append("-m %r" % m) self.write_sep("=", "%d tests deselected by %r" %( - len(self.stats['deselected']), self.config.option.keyword), bold=True) + len(self.stats['deselected']), " ".join(l)), bold=True) def repr_pythonversion(v=None): if v is None: diff --git a/doc/Makefile b/doc/Makefile index 4fbcc1251..bc965c7c4 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -40,7 +40,7 @@ clean: -rm -rf $(BUILDDIR)/* install: html - @rsync -avz _build/html/ pytest.org:/www/pytest.org/latest + @rsync -avz _build/html/ pytest.org:/www/pytest.org/2.2.0.dev7 installpdf: latexpdf @scp $(BUILDDIR)/latex/pytest.pdf pytest.org:/www/pytest.org/latest diff --git a/doc/index.txt b/doc/index.txt index 817cc3436..fa127f01b 100644 --- a/doc/index.txt +++ b/doc/index.txt @@ -26,8 +26,8 @@ Welcome to pytest! - **supports functional testing and complex test setups** - (new in 2.2) :ref:`durations` + - (much improved in 2.2) :ref:`marking and test selection ` - advanced :ref:`skip and xfail` - - generic :ref:`marking and test selection ` - can :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` - can :ref:`continuously re-run failing tests ` - many :ref:`builtin helpers ` diff --git a/doc/mark.txt b/doc/mark.txt index cee2e0cd0..71199d450 100644 --- a/doc/mark.txt +++ b/doc/mark.txt @@ -28,16 +28,34 @@ You can "mark" a test function with custom metadata like this:: @pytest.mark.webtest def test_send_http(): pass # perform some webtest test for your app + def test_something_quick(): + pass .. versionadded:: 2.2 -You can restrict a test run only tests marked with ``webtest`` like this:: +You can then restrict a test run to only run tests marked with ``webtest``:: - $ py.test -m webtest + $ py.test -v -m webtest + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.0.dev6 -- /Users/hpk/venv/0/bin/python + collecting ... collected 2 items + + test_server.py:3: test_send_http PASSED + + ===================== 1 tests deselected by "-m 'webtest'" ===================== + ==================== 1 passed, 1 deselected in 0.01 seconds ==================== Or the inverse, running all tests except the webtest ones:: - $ py.test -m "not webtest" + $ py.test -v -m "not webtest" + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.0.dev6 -- /Users/hpk/venv/0/bin/python + collecting ... collected 2 items + + test_server.py:6: test_something_quick PASSED + + =================== 1 tests deselected by "-m 'not webtest'" =================== + ==================== 1 passed, 1 deselected in 0.01 seconds ==================== Registering markers ------------------------------------- @@ -53,9 +71,19 @@ Registering markers for your test suite is simple:: markers = webtest: mark a test as a webtest. -You can ask which markers exist for your test suite:: +You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` markers:: $ py.test --markers + @pytest.mark.webtest: mark a test as a webtest. + + @pytest.mark.skipif(*conditions): skip the given test function if evaluation of all conditions has a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. + + @pytest.mark.xfail(*conditions, reason=None, run=True): mark the the test function as an expected failure. Optionally specify a reason and run=False if you don't even want to execute the test function. Any positional condition strings will be evaluated (like with skipif) and if one is False the marker will not be applied. + + @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + + @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + For an example on how to add and work markers from a plugin, see :ref:`adding a custom marker from a plugin`. @@ -118,39 +146,42 @@ methods defined in the module. Using ``-k TEXT`` to select tests ---------------------------------------------------- -You can use the ``-k`` command line option to select tests:: +You can use the ``-k`` command line option to only run tests with names that match the given argument:: - $ py.test -k webtest # running with the above defined examples yields - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.1.3 + $ py.test -k send_http # running with the above defined examples + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.0.dev6 collecting ... collected 4 items - test_mark.py .. - test_mark_classlevel.py .. + test_server.py . - ========================= 4 passed in 0.03 seconds ========================= + ===================== 3 tests deselected by '-ksend_http' ====================== + ==================== 1 passed, 3 deselected in 0.02 seconds ==================== And you can also run all tests except the ones that match the keyword:: - $ py.test -k-webtest - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.1.3 + $ py.test -k-send_http + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.0.dev6 collecting ... collected 4 items - ===================== 4 tests deselected by '-webtest' ===================== - ======================= 4 deselected in 0.02 seconds ======================= + test_mark_classlevel.py .. + test_server.py . + + ===================== 1 tests deselected by '-k-send_http' ===================== + ==================== 3 passed, 1 deselected in 0.03 seconds ==================== Or to only select the class:: $ py.test -kTestClass - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.1.3 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.0.dev6 collecting ... collected 4 items test_mark_classlevel.py .. - ==================== 2 tests deselected by 'TestClass' ===================== - ================== 2 passed, 2 deselected in 0.02 seconds ================== + ===================== 2 tests deselected by '-kTestClass' ====================== + ==================== 2 passed, 2 deselected in 0.02 seconds ==================== API reference for mark related objects ------------------------------------------------ diff --git a/setup.py b/setup.py index 54f677a53..a06d060aa 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.2.0.dev6', + version='2.2.0.dev7', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/test_mark.py b/testing/test_mark.py index 7c5fb13fd..031001835 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -114,6 +114,30 @@ def test_strict_prohibits_unregistered_markers(testdir): "*unregisteredmark*not*registered*", ]) +@pytest.mark.multi(spec=[ + ("xyz", ("test_one",)), + ("xyz and xyz2", ()), + ("xyz2", ("test_two",)), + ("xyz or xyz2", ("test_one", "test_two"),) +]) +def test_mark_option(spec, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.xyz + def test_one(): + pass + @pytest.mark.xyz2 + def test_two(): + pass + """) + opt, passed_result = spec + rec = testdir.inline_run("-m", 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) + + class TestFunctional: def test_mark_per_function(self, testdir):