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:
holger krekel 2010-05-21 18:11:47 +02:00
parent 67ec87e7f9
commit 4f7ef0b63f
7 changed files with 192 additions and 54 deletions

View File

@ -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.

View File

@ -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
----------------------------------------------------

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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