diff --git a/CHANGELOG b/CHANGELOG index 069fbfdd7..be7882470 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,14 @@ Changes between 1.3.0 and 1.3.1 to the underlying capturing functionality to avoid race conditions). +- fix issue89 - allow py.test.mark decorators to be used on classes + (class decorators were introduced with python2.6) + also allow to have multiple markers applied at class/module level + +- fix chaining of conditional skipif/xfail decorators - so it works now + as expected to use multiple @py.test.mark.skipif(condition) decorators, + including specific reporting which of the conditions lead to skipping. + - fix issue95: late-import zlib so that it's not required for general py.test startup. diff --git a/doc/test/plugin/mark.txt b/doc/test/plugin/mark.txt index 5a24dda44..4a193c68a 100644 --- a/doc/test/plugin/mark.txt +++ b/doc/test/plugin/mark.txt @@ -39,38 +39,43 @@ and later access it with ``test_receive.webtest.args[0] == 'triangular``. .. _`scoped-marking`: -Marking classes or modules +Marking whole classes or modules ---------------------------------------------------- -To mark all methods of a class set a ``pytestmark`` attribute like this:: +If you are programming with Python2.6 you may use ``py.test.mark`` decorators +with classes to apply markers to all its test methods:: + + @py.test.mark.webtest + class TestClass: + def test_startup(self): + ... + +This is equivalent to directly applying the decorator to the +``test_startup`` function. + +To remain compatible with Python2.5 you can instead set a +``pytestmark`` attribute on a TestClass like this:: import py class TestClass: pytestmark = py.test.mark.webtest -You can re-use the same markers that you would use for decorating -a function - in fact this marker decorator will be applied -to all test methods of the class. +or if you need to use multiple markers:: + + import py + + class TestClass: + pytestmark = [py.test.mark.webtest, pytest.mark.slowtest] You can also set a module level marker:: import py pytestmark = py.test.mark.webtest -in which case then the marker decorator will be applied to all functions and +in which case then it will be applied to all functions and methods defined in the module. -The order in which marker functions are called is this:: - - per-function (upon import of module already) - per-class - per-module - -Later called markers may overwrite previous key-value settings. -Positional arguments are all appended to the same 'args' list -of the Marker object. - Using "-k MARKNAME" to select tests ---------------------------------------------------- diff --git a/doc/test/plugin/skipping.txt b/doc/test/plugin/skipping.txt index 04e2ee19b..465dd4eeb 100644 --- a/doc/test/plugin/skipping.txt +++ b/doc/test/plugin/skipping.txt @@ -65,6 +65,18 @@ for skipping all methods of a test class based on platform:: # The ``pytestmark`` decorator will be applied to each test function. +If your code targets python2.6 or above you can also use the +skipif decorator with classes:: + + @py.test.mark.skipif("sys.platform == 'win32'") + class TestPosixCalls: + + def test_function(self): + # will not be setup or run under 'win32' platform + # + +It is fine in both situations to use multiple "skipif" decorators +on a single function. .. _`whole class- or module level`: mark.html#scoped-marking diff --git a/py/_plugin/pytest_mark.py b/py/_plugin/pytest_mark.py index 279a0b853..b029d52fe 100644 --- a/py/_plugin/pytest_mark.py +++ b/py/_plugin/pytest_mark.py @@ -34,38 +34,43 @@ and later access it with ``test_receive.webtest.args[0] == 'triangular``. .. _`scoped-marking`: -Marking classes or modules +Marking whole classes or modules ---------------------------------------------------- -To mark all methods of a class set a ``pytestmark`` attribute like this:: +If you are programming with Python2.6 you may use ``py.test.mark`` decorators +with classes to apply markers to all its test methods:: + + @py.test.mark.webtest + class TestClass: + def test_startup(self): + ... + +This is equivalent to directly applying the decorator to the +``test_startup`` function. + +To remain compatible with Python2.5 you can instead set a +``pytestmark`` attribute on a TestClass like this:: import py class TestClass: pytestmark = py.test.mark.webtest -You can re-use the same markers that you would use for decorating -a function - in fact this marker decorator will be applied -to all test methods of the class. +or if you need to use multiple markers:: + + import py + + class TestClass: + pytestmark = [py.test.mark.webtest, pytest.mark.slowtest] You can also set a module level marker:: import py pytestmark = py.test.mark.webtest -in which case then the marker decorator will be applied to all functions and +in which case then it will be applied to all functions and methods defined in the module. -The order in which marker functions are called is this:: - - per-function (upon import of module already) - per-class - per-module - -Later called markers may overwrite previous key-value settings. -Positional arguments are all appended to the same 'args' list -of the Marker object. - Using "-k MARKNAME" to select tests ---------------------------------------------------- @@ -105,15 +110,23 @@ class MarkDecorator: """ if passed a single callable argument: decorate it with mark info. otherwise add *args/**kwargs in-place to mark information. """ if args: - if len(args) == 1 and hasattr(args[0], '__call__'): - func = args[0] - holder = getattr(func, self.markname, None) - if holder is None: - holder = MarkInfo(self.markname, self.args, self.kwargs) - setattr(func, self.markname, holder) + func = args[0] + if len(args) == 1 and hasattr(func, '__call__') or \ + hasattr(func, '__bases__'): + if hasattr(func, '__bases__'): + l = func.__dict__.setdefault("pytestmark", []) + if not isinstance(l, list): + func.pytestmark = [l, self] + else: + l.append(self) else: - holder.kwargs.update(self.kwargs) - holder.args.extend(self.args) + holder = getattr(func, self.markname, None) + if holder is None: + holder = MarkInfo(self.markname, self.args, self.kwargs) + setattr(func, self.markname, holder) + else: + holder.kwargs.update(self.kwargs) + holder.args.extend(self.args) return func else: self.args.extend(args) @@ -147,6 +160,10 @@ def pytest_pycollect_makeitem(__multicall__, collector, name, obj): func = getattr(func, 'im_func', func) # py2 for parent in [x for x in (mod, cls) if x]: marker = getattr(parent.obj, 'pytestmark', None) - if isinstance(marker, MarkDecorator): - marker(func) + if marker is not None: + if not isinstance(marker, list): + marker = [marker] + for mark in marker: + if isinstance(mark, MarkDecorator): + mark(func) return item diff --git a/py/_plugin/pytest_skipping.py b/py/_plugin/pytest_skipping.py index 01548e5d3..6ea3154df 100644 --- a/py/_plugin/pytest_skipping.py +++ b/py/_plugin/pytest_skipping.py @@ -60,6 +60,19 @@ for skipping all methods of a test class based on platform:: # The ``pytestmark`` decorator will be applied to each test function. +If your code targets python2.6 or above you can equivalently use +the skipif decorator on classes:: + + @py.test.mark.skipif("sys.platform == 'win32'") + class TestPosixCalls: + + def test_function(self): + # will not be setup or run under 'win32' platform + # + +It is fine in general to apply multiple "skipif" decorators +on a single function - this means that if any of the conditions +apply the function will be skipped. .. _`whole class- or module level`: mark.html#scoped-marking @@ -144,17 +157,20 @@ class MarkEvaluator: def istrue(self): if self.holder: d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config} - self.result = True - for expr in self.holder.args: - self.expr = expr - if isinstance(expr, str): - result = cached_eval(self.item.config, expr, d) - else: - result = expr - if not result: - self.result = False + if self.holder.args: + self.result = False + for expr in self.holder.args: self.expr = expr - break + if isinstance(expr, str): + result = cached_eval(self.item.config, expr, d) + else: + result = expr + if result: + self.result = True + self.expr = expr + break + else: + self.result = True return getattr(self, 'result', False) def get(self, attr, default=None): diff --git a/testing/plugin/test_pytest_mark.py b/testing/plugin/test_pytest_mark.py index 95e3440b5..7f899f301 100644 --- a/testing/plugin/test_pytest_mark.py +++ b/testing/plugin/test_pytest_mark.py @@ -68,11 +68,41 @@ class TestFunctional: keywords = item.readkeywords() assert 'hello' in keywords - def test_mark_per_class(self, testdir): + def test_marklist_per_class(self, testdir): modcol = testdir.getmodulecol(""" import py class TestClass: - pytestmark = py.test.mark.hello + pytestmark = [py.test.mark.hello, py.test.mark.world] + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + clscol = modcol.collect()[0] + item = clscol.collect()[0].collect()[0] + keywords = item.readkeywords() + assert 'hello' in keywords + + def test_marklist_per_module(self, testdir): + modcol = testdir.getmodulecol(""" + import py + pytestmark = [py.test.mark.hello, py.test.mark.world] + class TestClass: + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + clscol = modcol.collect()[0] + item = clscol.collect()[0].collect()[0] + keywords = item.readkeywords() + assert 'hello' in keywords + assert 'world' in keywords + + @py.test.mark.skipif("sys.version_info < (2,6)") + def test_mark_per_class_decorator(self, testdir): + modcol = testdir.getmodulecol(""" + import py + @py.test.mark.hello + class TestClass: def test_func(self): assert TestClass.test_func.hello """) @@ -81,6 +111,23 @@ class TestFunctional: keywords = item.readkeywords() assert 'hello' in keywords + @py.test.mark.skipif("sys.version_info < (2,6)") + def test_mark_per_class_decorator_plus_existing_dec(self, testdir): + modcol = testdir.getmodulecol(""" + import py + @py.test.mark.hello + class TestClass: + pytestmark = py.test.mark.world + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + clscol = modcol.collect()[0] + item = clscol.collect()[0].collect()[0] + keywords = item.readkeywords() + assert 'hello' in keywords + assert 'world' in keywords + def test_merging_markers(self, testdir): p = testdir.makepyfile(""" import py diff --git a/testing/plugin/test_pytest_skipping.py b/testing/plugin/test_pytest_skipping.py index ead1dd2c3..2058a5fb0 100644 --- a/testing/plugin/test_pytest_skipping.py +++ b/testing/plugin/test_pytest_skipping.py @@ -52,6 +52,39 @@ class TestEvaluator: assert expl == "hello world" assert ev.get("attr") == 2 + def test_marked_one_arg_twice(self, testdir): + lines = [ + '''@py.test.mark.skipif("not hasattr(os, 'murks')")''', + '''@py.test.mark.skipif("hasattr(os, 'murks')")''' + ] + for i in range(0, 2): + item = testdir.getitem(""" + import py + %s + %s + def test_func(): + pass + """ % (lines[i], lines[(i+1) %2])) + ev = MarkEvaluator(item, 'skipif') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: not hasattr(os, 'murks')" + + def test_marked_one_arg_twice2(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.skipif("hasattr(os, 'murks')") + @py.test.mark.skipif("not hasattr(os, 'murks')") + def test_func(): + pass + """) + ev = MarkEvaluator(item, 'skipif') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: not hasattr(os, 'murks')" + def test_skipif_class(self, testdir): item, = testdir.getitems(""" import py