From ccc1b21ebdcb2d94963b580d8fa11da00fc98b1e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 28 Dec 2011 15:47:18 +0000 Subject: [PATCH] internally keep multiple applications of the same markers as separate entities such that the new iter() API can iterate over pytest.mark function attributes, getting all such applications. See added example for more info. --- _pytest/mark.py | 15 ++++++- doc/example/markers.txt | 92 +++++++++++++++++++++++++++++------------ testing/test_mark.py | 25 +++++++++++ 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index 1c22e72dd..842272e3d 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -191,8 +191,7 @@ class MarkDecorator: holder = MarkInfo(self.markname, self.args, self.kwargs) setattr(func, self.markname, holder) else: - holder.kwargs.update(self.kwargs) - holder.args += self.args + holder.add(self.args, self.kwargs) return func kw = self.kwargs.copy() kw.update(kwargs) @@ -208,11 +207,23 @@ class MarkInfo: self.args = args #: keyword argument dictionary, empty if nothing specified self.kwargs = kwargs + self._arglist = [(args, kwargs.copy())] def __repr__(self): return "" % ( self.name, self.args, self.kwargs) + def add(self, args, kwargs): + """ add a MarkInfo with the given args and kwargs. """ + self._arglist.append((args, kwargs)) + self.args += args + self.kwargs.update(kwargs) + + def __iter__(self): + """ yield MarkInfo objects each relating to a marking-call. """ + for args, kwargs in self._arglist: + yield MarkInfo(self.name, args, kwargs) + def pytest_itemcollected(item): if not isinstance(item, pytest.Function): return diff --git a/doc/example/markers.txt b/doc/example/markers.txt index bce273414..d3a71a7d8 100644 --- a/doc/example/markers.txt +++ b/doc/example/markers.txt @@ -25,26 +25,26 @@ You can "mark" a test function with custom metadata like this:: You can then restrict a test run to only run tests marked with ``webtest``:: $ py.test -v -m webtest - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 -- /Users/hpk/venv/1/bin/python + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 -- /Users/hpk/venv/1/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 ================== + ===================== 1 tests deselected by "-m 'webtest'" ===================== + ==================== 1 passed, 1 deselected in 0.03 seconds ==================== Or the inverse, running all tests except the webtest ones:: $ py.test -v -m "not webtest" - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 -- /Users/hpk/venv/1/bin/python + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 -- /Users/hpk/venv/1/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 ================== + =================== 1 tests deselected by "-m 'not webtest'" =================== + ==================== 1 passed, 1 deselected in 0.03 seconds ==================== Registering markers ------------------------------------- @@ -134,6 +134,7 @@ You can also set a module level marker:: in which case it will be applied to all functions and methods defined in the module. + Using ``-k TEXT`` to select tests ---------------------------------------------------- @@ -141,39 +142,39 @@ You can use the ``-k`` command line option to only run tests with names matching the given argument:: $ py.test -k send_http # running with the above defined examples - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 collecting ... collected 4 items test_server.py . - =================== 3 tests deselected by '-ksend_http' ==================== - ================== 1 passed, 3 deselected in 0.02 seconds ================== + ===================== 3 tests deselected by '-ksend_http' ====================== + ==================== 1 passed, 3 deselected in 0.06 seconds ==================== And you can also run all tests except the ones that match the keyword:: $ py.test -k-send_http - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 collecting ... collected 4 items test_mark_classlevel.py .. test_server.py . - =================== 1 tests deselected by '-k-send_http' =================== - ================== 3 passed, 1 deselected in 0.03 seconds ================== + ===================== 1 tests deselected by '-k-send_http' ===================== + ==================== 3 passed, 1 deselected in 0.05 seconds ==================== Or to only select the class:: $ py.test -kTestClass - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 collecting ... collected 4 items test_mark_classlevel.py .. - =================== 2 tests deselected by '-kTestClass' ==================== - ================== 2 passed, 2 deselected in 0.02 seconds ================== + ===================== 2 tests deselected by '-kTestClass' ====================== + ==================== 2 passed, 2 deselected in 0.04 seconds ==================== .. _`adding a custom marker from a plugin`: @@ -221,24 +222,24 @@ and an example invocations specifying a different environment than what the test needs:: $ py.test -E stage2 - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 collecting ... collected 1 items test_someenv.py s - ======================== 1 skipped in 0.02 seconds ========================= + ========================== 1 skipped in 0.03 seconds =========================== and here is one that specifies exactly the environment needed:: $ py.test -E stage1 - =========================== test session starts ============================ - platform darwin -- Python 2.7.1 -- pytest-2.2.1 + ============================= test session starts ============================== + platform darwin -- Python 2.7.1 -- pytest-2.2.2.dev3 collecting ... collected 1 items test_someenv.py . - ========================= 1 passed in 0.01 seconds ========================= + =========================== 1 passed in 0.03 seconds =========================== The ``--markers`` option always gives you a list of available markers:: @@ -254,4 +255,43 @@ The ``--markers`` option always gives you a list of available markers:: @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. + +Reading markers which were set from multiple places +---------------------------------------------------- + +.. versionadded: 2.2.2 + +If you are heavily using markers in your test suite you may encounter the case where a marker is applied several times to a test function. From plugin +code you can read over all such settings. Example:: + + # content of test_mark_three_times.py + import pytest + pytestmark = pytest.mark.glob("module", x=1) + + @pytest.mark.glob("class", x=2) + class TestClass: + @pytest.mark.glob("function", x=3) + def test_something(self): + pass + +Here we have the marker "glob" applied three times to the same +test function. From a conftest file we can read it like this:: + + # content of conftest.py + + def pytest_runtest_setup(item): + g = getattr(item.obj, 'glob', None) + if g is not None: + for info in g: + print ("glob args=%s kwargs=%s" %(info.args, info.kwargs)) + +Let's run this without capturing output and see what we get:: + + $ py.test -q -s + collecting ... collected 2 items + .. + 2 passed in 0.04 seconds + glob args=('function',) kwargs={'x': 3} + glob args=('module',) kwargs={'x': 1} + glob args=('class',) kwargs={'x': 2} diff --git a/testing/test_mark.py b/testing/test_mark.py index 031001835..2c8b52db5 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -230,6 +230,14 @@ class TestFunctional: assert marker.args == ("pos0", "pos1") assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4} + # test the new __iter__ interface + l = list(marker) + assert len(l) == 3 + assert l[0].args == ("pos0",) + pytest.xfail(reason="needs reordering of parametrize transfermarks") + assert l[1].args == () + assert l[2].args == ("pos1", ) + def test_mark_other(self, testdir): pytest.raises(TypeError, ''' testdir.getitem(""" @@ -259,6 +267,23 @@ class TestFunctional: "keyword: *hello*" ]) + def test_merging_markers_two_functions(self, testdir): + p = testdir.makepyfile(""" + import pytest + @pytest.mark.hello("pos1", z=4) + @pytest.mark.hello("pos0", z=3) + def test_func(self): + pass + """) + items, rec = testdir.inline_genitems(p) + item, = items + keywords = item.keywords + marker = keywords['hello'] + l = list(marker) + assert len(l) == 2 + assert l[0].args == ("pos0",) + assert l[1].args == ("pos1",) + class TestKeywordSelection: def test_select_simple(self, testdir):