From bc8ee95e72bbcdfff2c4c72de624d31a35cbdc9e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 11 Nov 2011 22:56:11 +0000 Subject: [PATCH] add ini-file "markers" option and a cmdline option "--markers" to show defined markers. Add "skipif", "xfail" etc. to the set of builtin markers shown with the --markers option. --- CHANGELOG | 14 ++++++++++-- _pytest/__init__.py | 2 +- _pytest/core.py | 8 +++++++ _pytest/main.py | 3 +++ _pytest/mark.py | 42 +++++++++++++++++++++++++++++++++-- _pytest/skipping.py | 15 +++++++++++++ doc/mark.txt | 6 ++--- setup.py | 2 +- testing/conftest.py | 4 ++++ testing/test_core.py | 7 ++++++ testing/test_mark.py | 47 ++++++++++++++++++++++++++++++++++++++++ testing/test_skipping.py | 7 ++++++ 12 files changed, 148 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2c28ae2c8..29436e084 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,18 @@ Changes between 2.1.3 and XXX 2.2.0 ---------------------------------------- -- new feature to help optimizing your tests: --durations=N option for - displaying N slowest test calls and setup/teardown methods. +- introduce registration for "pytest.mark.*" helpers via ini-files + or through plugin hooks. Also introduce a "--strict" option which + will treat unregistered markers as errors + 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.) +- new feature to help optimizing the speed of your tests: + --durations=N option for displaying N slowest test calls + and setup/teardown methods. - fix and cleanup pytest's own test suite to not leak FDs - fix issue83: link to generated funcarg list - fix issue74: pyarg module names are now checked against imp.find_module false positives diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 264b6f021..c0fe93997 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.2.0.dev5' +__version__ = '2.2.0.dev6' diff --git a/_pytest/core.py b/_pytest/core.py index 8ccae40c8..eabade7ec 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -211,6 +211,14 @@ class PluginManager(object): self.register(mod, modname) self.consider_module(mod) + def pytest_configure(self, config): + config.addinivalue_line("markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible.") + config.addinivalue_line("markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible.") + def pytest_plugin_registered(self, plugin): import pytest dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} diff --git a/_pytest/main.py b/_pytest/main.py index 4d4852105..819e3c124 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -29,6 +29,9 @@ def pytest_addoption(parser): action="store", type="int", dest="maxfail", default=0, help="exit after first num failures or errors.") + group._addoption('--strict', action="store_true", + help="run pytest in strict mode, warnings become errors.") + group = parser.getgroup("collect", "collection") group.addoption('--collectonly', action="store_true", dest="collectonly", diff --git a/_pytest/mark.py b/_pytest/mark.py index 6cc8edcdb..96ad2af6a 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -14,6 +14,24 @@ def pytest_addoption(parser): "Terminate expression with ':' to make the first match match " "all subsequent tests (usually file-order). ") + group.addoption("--markers", action="store_true", help= + "show markers (builtin, plugin and per-project ones).") + + parser.addini("markers", "markers for test functions", 'linelist') + +def pytest_cmdline_main(config): + if config.option.markers: + config.pluginmanager.do_configure(config) + tw = py.io.TerminalWriter() + for line in config.getini("markers"): + name, rest = line.split(":", 1) + tw.write("@pytest.mark.%s:" % name, bold=True) + tw.line(rest) + tw.line() + config.pluginmanager.do_unconfigure(config) + return 0 +pytest_cmdline_main.tryfirst = True + def pytest_collection_modifyitems(items, config): keywordexpr = config.option.keyword if not keywordexpr: @@ -37,13 +55,17 @@ def pytest_collection_modifyitems(items, config): config.hook.pytest_deselected(items=deselected) items[:] = remaining +def pytest_configure(config): + if config.option.strict: + pytest.mark._config = config + def skipbykeyword(colitem, keywordexpr): """ return True if they given keyword expression means to skip this collector/item. """ if not keywordexpr: return - + itemkeywords = getkeywords(colitem) for key in filter(None, keywordexpr.split()): eor = key[:1] == '-' @@ -77,15 +99,31 @@ class MarkGenerator: @py.test.mark.slowtest def test_function(): pass - + will set a 'slowtest' :class:`MarkInfo` object on the ``test_function`` object. """ def __getattr__(self, name): if name[0] == "_": raise AttributeError(name) + if hasattr(self, '_config'): + self._check(name) return MarkDecorator(name) + def _check(self, name): + try: + if name in self._markers: + return + except AttributeError: + pass + self._markers = l = set() + for line in self._config.getini("markers"): + beginning = line.split(":", 1) + x = beginning[0].split("(", 1)[0] + l.add(x) + if name not in self._markers: + raise AttributeError("%r not a registered marker" % (name,)) + class MarkDecorator: """ A decorator for test functions and test classes. When applied it will create :class:`MarkInfo` objects which may be diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 6a1ddece4..6098c81a4 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -9,6 +9,21 @@ def pytest_addoption(parser): action="store_true", dest="runxfail", default=False, help="run tests even if they are marked xfail") +def pytest_configure(config): + config.addinivalue_line("markers", + "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. " + ) + config.addinivalue_line("markers", + "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." + ) + def pytest_namespace(): return dict(xfail=xfail) diff --git a/doc/mark.txt b/doc/mark.txt index ae2b26da9..cee2e0cd0 100644 --- a/doc/mark.txt +++ b/doc/mark.txt @@ -68,9 +68,9 @@ For an example on how to add and work markers from a plugin, see * asking for existing markers via ``py.test --markers`` gives good output - * typos in function markers can be treated as an error if you use - the :ref:`--strict` option. Later versions of py.test might treat - non-registered markers as an error by default. + * typos in function markers are treated as an error if you use + the ``--strict`` option. Later versions of py.test are probably + going to treat non-registered markers as an error. .. _`scoped-marking`: diff --git a/setup.py b/setup.py index 7ef9908e9..54f677a53 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.dev5', + version='2.2.0.dev6', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/conftest.py b/testing/conftest.py index e020264a7..b73936fc1 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -12,6 +12,10 @@ def pytest_addoption(parser): help=("run FD checks if lsof is available")) def pytest_configure(config): + config.addinivalue_line("markers", + "multi(arg=[value1,value2, ...]): call the test function " + "multiple times with arg=value1, then with arg=value2, ... " + ) if config.getvalue("lsof"): try: out = py.process.cmdexec("lsof -p %d" % pid) diff --git a/testing/test_core.py b/testing/test_core.py index b122cf689..09497ef7c 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -644,3 +644,10 @@ class TestTracer: assert "1" in tags assert "2" in tags assert args == (42,) + +def test_default_markers(testdir): + result = testdir.runpytest("--markers") + result.stdout.fnmatch_lines([ + "*tryfirst*first*", + "*trylast*last*", + ]) diff --git a/testing/test_mark.py b/testing/test_mark.py index b0faca46f..7c5fb13fd 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -68,7 +68,54 @@ class TestMark: assert 'reason' not in g.some.kwargs assert g.some.kwargs['reason2'] == "456" + +def test_ini_markers(testdir): + testdir.makeini(""" + [pytest] + markers = + a1: this is a webtest marker + a2: this is a smoke marker + """) + testdir.makepyfile(""" + def test_markers(pytestconfig): + markers = pytestconfig.getini("markers") + print (markers) + assert len(markers) >= 2 + assert markers[0].startswith("a1:") + assert markers[1].startswith("a2:") + """) + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + +def test_markers_option(testdir): + testdir.makeini(""" + [pytest] + markers = + a1: this is a webtest marker + a1some: another marker + """) + result = testdir.runpytest("--markers", ) + result.stdout.fnmatch_lines([ + "*a1*this is a webtest*", + "*a1some*another marker", + ]) + + +def test_strict_prohibits_unregistered_markers(testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.unregisteredmark + def test_hello(): + pass + """) + result = testdir.runpytest("--strict") + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*unregisteredmark*not*registered*", + ]) + class TestFunctional: + def test_mark_per_function(self, testdir): p = testdir.makepyfile(""" import pytest diff --git a/testing/test_skipping.py b/testing/test_skipping.py index e6a6bce0b..3de87f65b 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -549,3 +549,10 @@ def test_direct_gives_error(testdir): ]) +def test_default_markers(testdir): + result = testdir.runpytest("--markers") + result.stdout.fnmatch_lines([ + "*skipif(*conditions)*skip*", + "*xfail(*conditions, reason=None, run=True)*expected failure*", + ]) +