fix issue89 - allow py.test.mark decorators to be used with classes
(if you are using >=python2.6) also allow to have multiple markers applied at class level and test and fix a bug with chained skip/xfail decorators: if any of the conditions is true a test will be skipped/xfailed with a explanation which condition evaluated to true. --HG-- branch : trunk
This commit is contained in:
parent
67ec87e7f9
commit
4f7ef0b63f
|
@ -6,6 +6,14 @@ Changes between 1.3.0 and 1.3.1
|
||||||
to the underlying capturing functionality to avoid race
|
to the underlying capturing functionality to avoid race
|
||||||
conditions).
|
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
|
- fix issue95: late-import zlib so that it's not required
|
||||||
for general py.test startup.
|
for general py.test startup.
|
||||||
|
|
||||||
|
|
|
@ -39,38 +39,43 @@ and later access it with ``test_receive.webtest.args[0] == 'triangular``.
|
||||||
|
|
||||||
.. _`scoped-marking`:
|
.. _`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
|
import py
|
||||||
|
|
||||||
class TestClass:
|
class TestClass:
|
||||||
pytestmark = py.test.mark.webtest
|
pytestmark = py.test.mark.webtest
|
||||||
|
|
||||||
You can re-use the same markers that you would use for decorating
|
or if you need to use multiple markers::
|
||||||
a function - in fact this marker decorator will be applied
|
|
||||||
to all test methods of the class.
|
import py
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
pytestmark = [py.test.mark.webtest, pytest.mark.slowtest]
|
||||||
|
|
||||||
You can also set a module level marker::
|
You can also set a module level marker::
|
||||||
|
|
||||||
import py
|
import py
|
||||||
pytestmark = py.test.mark.webtest
|
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.
|
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
|
Using "-k MARKNAME" to select tests
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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
|
.. _`whole class- or module level`: mark.html#scoped-marking
|
||||||
|
|
||||||
|
|
|
@ -34,38 +34,43 @@ and later access it with ``test_receive.webtest.args[0] == 'triangular``.
|
||||||
|
|
||||||
.. _`scoped-marking`:
|
.. _`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
|
import py
|
||||||
|
|
||||||
class TestClass:
|
class TestClass:
|
||||||
pytestmark = py.test.mark.webtest
|
pytestmark = py.test.mark.webtest
|
||||||
|
|
||||||
You can re-use the same markers that you would use for decorating
|
or if you need to use multiple markers::
|
||||||
a function - in fact this marker decorator will be applied
|
|
||||||
to all test methods of the class.
|
import py
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
pytestmark = [py.test.mark.webtest, pytest.mark.slowtest]
|
||||||
|
|
||||||
You can also set a module level marker::
|
You can also set a module level marker::
|
||||||
|
|
||||||
import py
|
import py
|
||||||
pytestmark = py.test.mark.webtest
|
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.
|
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
|
Using "-k MARKNAME" to select tests
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
@ -105,15 +110,23 @@ class MarkDecorator:
|
||||||
""" if passed a single callable argument: decorate it with mark info.
|
""" if passed a single callable argument: decorate it with mark info.
|
||||||
otherwise add *args/**kwargs in-place to mark information. """
|
otherwise add *args/**kwargs in-place to mark information. """
|
||||||
if args:
|
if args:
|
||||||
if len(args) == 1 and hasattr(args[0], '__call__'):
|
func = args[0]
|
||||||
func = args[0]
|
if len(args) == 1 and hasattr(func, '__call__') or \
|
||||||
holder = getattr(func, self.markname, None)
|
hasattr(func, '__bases__'):
|
||||||
if holder is None:
|
if hasattr(func, '__bases__'):
|
||||||
holder = MarkInfo(self.markname, self.args, self.kwargs)
|
l = func.__dict__.setdefault("pytestmark", [])
|
||||||
setattr(func, self.markname, holder)
|
if not isinstance(l, list):
|
||||||
|
func.pytestmark = [l, self]
|
||||||
|
else:
|
||||||
|
l.append(self)
|
||||||
else:
|
else:
|
||||||
holder.kwargs.update(self.kwargs)
|
holder = getattr(func, self.markname, None)
|
||||||
holder.args.extend(self.args)
|
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
|
return func
|
||||||
else:
|
else:
|
||||||
self.args.extend(args)
|
self.args.extend(args)
|
||||||
|
@ -147,6 +160,10 @@ def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
|
||||||
func = getattr(func, 'im_func', func) # py2
|
func = getattr(func, 'im_func', func) # py2
|
||||||
for parent in [x for x in (mod, cls) if x]:
|
for parent in [x for x in (mod, cls) if x]:
|
||||||
marker = getattr(parent.obj, 'pytestmark', None)
|
marker = getattr(parent.obj, 'pytestmark', None)
|
||||||
if isinstance(marker, MarkDecorator):
|
if marker is not None:
|
||||||
marker(func)
|
if not isinstance(marker, list):
|
||||||
|
marker = [marker]
|
||||||
|
for mark in marker:
|
||||||
|
if isinstance(mark, MarkDecorator):
|
||||||
|
mark(func)
|
||||||
return item
|
return item
|
||||||
|
|
|
@ -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.
|
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
|
.. _`whole class- or module level`: mark.html#scoped-marking
|
||||||
|
|
||||||
|
@ -144,17 +157,20 @@ class MarkEvaluator:
|
||||||
def istrue(self):
|
def istrue(self):
|
||||||
if self.holder:
|
if self.holder:
|
||||||
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
|
||||||
self.result = True
|
if self.holder.args:
|
||||||
for expr in self.holder.args:
|
self.result = False
|
||||||
self.expr = expr
|
for expr in self.holder.args:
|
||||||
if isinstance(expr, str):
|
|
||||||
result = cached_eval(self.item.config, expr, d)
|
|
||||||
else:
|
|
||||||
result = expr
|
|
||||||
if not result:
|
|
||||||
self.result = False
|
|
||||||
self.expr = expr
|
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)
|
return getattr(self, 'result', False)
|
||||||
|
|
||||||
def get(self, attr, default=None):
|
def get(self, attr, default=None):
|
||||||
|
|
|
@ -68,11 +68,41 @@ class TestFunctional:
|
||||||
keywords = item.readkeywords()
|
keywords = item.readkeywords()
|
||||||
assert 'hello' in keywords
|
assert 'hello' in keywords
|
||||||
|
|
||||||
def test_mark_per_class(self, testdir):
|
def test_marklist_per_class(self, testdir):
|
||||||
modcol = testdir.getmodulecol("""
|
modcol = testdir.getmodulecol("""
|
||||||
import py
|
import py
|
||||||
class TestClass:
|
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):
|
def test_func(self):
|
||||||
assert TestClass.test_func.hello
|
assert TestClass.test_func.hello
|
||||||
""")
|
""")
|
||||||
|
@ -81,6 +111,23 @@ class TestFunctional:
|
||||||
keywords = item.readkeywords()
|
keywords = item.readkeywords()
|
||||||
assert 'hello' in keywords
|
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):
|
def test_merging_markers(self, testdir):
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
import py
|
import py
|
||||||
|
|
|
@ -52,6 +52,39 @@ class TestEvaluator:
|
||||||
assert expl == "hello world"
|
assert expl == "hello world"
|
||||||
assert ev.get("attr") == 2
|
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):
|
def test_skipif_class(self, testdir):
|
||||||
item, = testdir.getitems("""
|
item, = testdir.getitems("""
|
||||||
import py
|
import py
|
||||||
|
|
Loading…
Reference in New Issue