diff --git a/_pytest/python.py b/_pytest/python.py index 529e0d688..e55fe2148 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -4,6 +4,7 @@ import inspect import sys import pytest from _pytest.main import getfslineno +from _pytest.mark import MarkDecorator, MarkInfo from _pytest.monkeypatch import monkeypatch from py._code.code import TerminalRepr @@ -565,11 +566,13 @@ class CallSpec2(object): self._globalid_args = set() self._globalparam = _notexists self._arg2scopenum = {} # used for sorting parametrized resources + self.keywords = {} def copy(self, metafunc): cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) + cs.keywords.update(self.keywords) cs._arg2scopenum.update(self._arg2scopenum) cs._idlist = list(self._idlist) cs._globalid = self._globalid @@ -593,7 +596,7 @@ class CallSpec2(object): def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtype, argnames, valset, id, scopenum=0): + def setmulti(self, valtype, argnames, valset, id, keywords, scopenum=0): for arg,val in zip(argnames, valset): self._checkargnotcontained(arg) getattr(self, valtype)[arg] = val @@ -605,6 +608,7 @@ class CallSpec2(object): if val is _notexists: self._emptyparamspecified = True self._idlist.append(id) + self.keywords.update(keywords) def setall(self, funcargs, id, param): for x in funcargs: @@ -673,6 +677,18 @@ class Metafunc(FuncargnamesCompatAttr): if not argvalues: argvalues = [(_notexists,) * len(argnames)] + # these marks/keywords will be applied in Function init + newkeywords = {} + for i, argval in enumerate(argvalues): + newkeywords[i] = {} + if isinstance(argval, MarkDecorator): + # convert into a mark without the test content mixed in + newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) + newkeywords[i] = {newmark.markname: newmark} + + argvalues = [av.args[-1] if isinstance(av, MarkDecorator) else av + for av in argvalues] + if scope is None: scope = "subfunction" scopenum = scopes.index(scope) @@ -691,7 +707,7 @@ class Metafunc(FuncargnamesCompatAttr): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) newcallspec.setmulti(valtype, argnames, valset, ids[i], - scopenum) + newkeywords[i], scopenum) newcalls.append(newcallspec) self._calls = newcalls @@ -908,6 +924,9 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): for name, val in (py.builtin._getfuncdict(self.obj) or {}).items(): self.keywords[name] = val + if callspec: + for name, val in callspec.keywords.items(): + self.keywords[name] = val if keywords: for name, val in keywords.items(): self.keywords[name] = val diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 60247212f..16f2da493 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -577,4 +577,175 @@ class TestMetafuncFunctional: "*3 passed*" ]) + @pytest.mark.issue308 + def test_mark_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.foo + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.bar((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + items = testdir.getitems(s) + assert len(items) == 3 + for item in items: + assert 'foo' in item.keywords + assert 'bar' not in items[0].keywords + assert 'bar' in items[1].keywords + assert 'bar' not in items[2].keywords + + @pytest.mark.issue308 + def test_select_individual_parametrize_instance_based_on_mark(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.foo((2, 3)), + (3, 4), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + rec = testdir.inline_run("-m", 'foo') + passed, skipped, fail = rec.listoutcomes() + assert len(passed) == 1 + assert len(skipped) == 0 + assert len(fail) == 0 + + @pytest.mark.xfail("is this important to support??") + @pytest.mark.issue308 + def test_nested_marks_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.foo(pytest.mark.bar((1, 3))), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + items = testdir.getitems(s) + assert len(items) == 3 + for mark in ['foo', 'bar']: + assert mark not in items[0].keywords + assert mark in items[1].keywords + assert mark not in items[2].keywords + + @pytest.mark.xfail(reason="is this important to support??") + @pytest.mark.issue308 + def test_nested_marks_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + mastermark = pytest.mark.foo(pytest.mark.bar) + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + mastermark((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + items = testdir.getitems(s) + assert len(items) == 3 + for mark in ['foo', 'bar']: + assert mark not in items[0].keywords + assert mark in items[1].keywords + assert mark not in items[2].keywords + + @pytest.mark.issue308 + def test_simple_xfail_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xfail is skip?? + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_arg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_kwarg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail(reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_arg_and_kwarg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0", reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_is_xpass_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0", reason="some bug")((2, 3)), + (3, 4), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xpass is fail, obviously :) + reprec.assertoutcome(passed=2, failed=1)