allow unittest test functions to work with the "pytestmark" mechanism
by refactoring mark/keyword handling and initialization --HG-- branch : trunk
This commit is contained in:
parent
a6f10a6d80
commit
4480401119
|
@ -23,6 +23,8 @@ Changes between 1.3.4 and 2.0.0dev0
|
|||
- "xpass" (unexpected pass) tests don't cause exitcode!=0
|
||||
- fix issue131 / issue60 - importing doctests in __init__ files used as namespace packages
|
||||
- fix issue93 stdout/stderr is captured while importing conftest.py
|
||||
- fix bug: unittest collected functions now also can have "pytestmark"
|
||||
applied at class/module level
|
||||
|
||||
Changes between 1.3.3 and 1.3.4
|
||||
----------------------------------------------
|
||||
|
|
|
@ -5,7 +5,7 @@ see http://pytest.org for documentation and details
|
|||
|
||||
(c) Holger Krekel and others, 2004-2010
|
||||
"""
|
||||
__version__ = '2.0.0.dev7'
|
||||
__version__ = '2.0.0.dev8'
|
||||
|
||||
__all__ = ['config', 'cmdline']
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: "
|
|||
"%s is too old, remove or upgrade 'py'" % (py.__version__))
|
||||
|
||||
default_plugins = (
|
||||
"config session terminal python runner pdb capture mark skipping tmpdir "
|
||||
"monkeypatch recwarn pastebin unittest helpconfig nose assertion genscript "
|
||||
"config session terminal python runner pdb capture unittest mark skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
|
||||
"junitxml doctest").split()
|
||||
|
||||
IMPORTPREFIX = "pytest_"
|
||||
|
@ -282,7 +282,7 @@ class MultiCall:
|
|||
kwargs[argname] = self
|
||||
return kwargs
|
||||
|
||||
def varnames(func, cache={}):
|
||||
def varnames(func):
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
ismethod = inspect.ismethod(func)
|
||||
|
|
|
@ -7,11 +7,12 @@ def pytest_namespace():
|
|||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('-k',
|
||||
action="store", dest="keyword", default='',
|
||||
help="only run test items matching the given "
|
||||
"space separated keywords. precede a keyword with '-' to negate. "
|
||||
"Terminate the expression with ':' to treat a match as a signal "
|
||||
"to run all subsequent tests. ")
|
||||
action="store", dest="keyword", default='', metavar="KEYWORDEXPR",
|
||||
help="only run tests which match given keyword expression. "
|
||||
"An expression consists of space-separated terms. "
|
||||
"Each term must match. Precede a term with '-' to negate. "
|
||||
"Terminate expression with ':' to make the first match match "
|
||||
"all subsequent tests (usually file-order). ")
|
||||
|
||||
def pytest_collection_modifyitems(items, config):
|
||||
keywordexpr = config.option.keyword
|
||||
|
@ -42,32 +43,31 @@ def skipbykeyword(colitem, keywordexpr):
|
|||
"""
|
||||
if not keywordexpr:
|
||||
return
|
||||
chain = colitem.listchain()
|
||||
|
||||
itemkeywords = getkeywords(colitem)
|
||||
for key in filter(None, keywordexpr.split()):
|
||||
eor = key[:1] == '-'
|
||||
if eor:
|
||||
key = key[1:]
|
||||
if not (eor ^ matchonekeyword(key, chain)):
|
||||
if not (eor ^ matchonekeyword(key, itemkeywords)):
|
||||
return True
|
||||
|
||||
def matchonekeyword(key, chain):
|
||||
elems = key.split(".")
|
||||
# XXX O(n^2), anyone cares?
|
||||
chain = [item.keywords for item in chain if item.keywords]
|
||||
for start, _ in enumerate(chain):
|
||||
if start + len(elems) > len(chain):
|
||||
return False
|
||||
for num, elem in enumerate(elems):
|
||||
for keyword in chain[num + start]:
|
||||
ok = False
|
||||
if elem in keyword:
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
def getkeywords(node):
|
||||
keywords = {}
|
||||
while node is not None:
|
||||
keywords.update(node.keywords)
|
||||
node = node.parent
|
||||
return keywords
|
||||
|
||||
|
||||
def matchonekeyword(key, itemkeywords):
|
||||
for elem in key.split("."):
|
||||
for kw in itemkeywords:
|
||||
if elem in kw:
|
||||
break
|
||||
if num == len(elems) - 1 and ok:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
class MarkGenerator:
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
|
@ -155,21 +155,22 @@ class MarkInfo:
|
|||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self._name, self.args, self.kwargs)
|
||||
|
||||
def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
|
||||
item = __multicall__.execute()
|
||||
if isinstance(item, py.test.collect.Function):
|
||||
cls = collector.getparent(py.test.collect.Class)
|
||||
mod = collector.getparent(py.test.collect.Module)
|
||||
func = item.obj
|
||||
func = getattr(func, '__func__', func) # py3
|
||||
func = getattr(func, 'im_func', func) # py2
|
||||
for parent in [x for x in (mod, cls) if x]:
|
||||
marker = getattr(parent.obj, 'pytestmark', None)
|
||||
def pytest_log_itemcollect(item):
|
||||
if not isinstance(item, py.test.collect.Function):
|
||||
return
|
||||
try:
|
||||
func = item.obj.__func__
|
||||
except AttributeError:
|
||||
func = getattr(item.obj, 'im_func', item.obj)
|
||||
pyclasses = (py.test.collect.Class, py.test.collect.Module)
|
||||
for node in item.listchain():
|
||||
if isinstance(node, pyclasses):
|
||||
marker = getattr(node.obj, 'pytestmark', None)
|
||||
if marker is not None:
|
||||
if not isinstance(marker, list):
|
||||
marker = [marker]
|
||||
for mark in marker:
|
||||
if isinstance(mark, MarkDecorator):
|
||||
if isinstance(marker, list):
|
||||
for mark in marker:
|
||||
mark(func)
|
||||
item.keywords.update(py.builtin._getfuncdict(func) or {})
|
||||
return item
|
||||
else:
|
||||
marker(func)
|
||||
node = node.parent
|
||||
item.keywords.update(py.builtin._getfuncdict(func))
|
||||
|
|
|
@ -354,13 +354,10 @@ class TmpTestdir:
|
|||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
modcol = self.getmodulecol(source)
|
||||
moditems = modcol.collect()
|
||||
for item in modcol.collect():
|
||||
for item in self.getitems(source):
|
||||
if item.name == funcname:
|
||||
return item
|
||||
else:
|
||||
assert 0, "%r item not found in module:\n%s" %(funcname, source)
|
||||
assert 0, "%r item not found in module:\n%s" %(funcname, source)
|
||||
|
||||
def getitems(self, source):
|
||||
modcol = self.getmodulecol(source)
|
||||
|
|
|
@ -136,6 +136,7 @@ class PyCollectorMixin(PyobjMixin, pytest.collect.Collector):
|
|||
|
||||
def collect(self):
|
||||
# NB. we avoid random getattrs and peek in the __dict__ instead
|
||||
# (XXX originally introduced from a PyPy need, still true?)
|
||||
dicts = [getattr(self.obj, '__dict__', {})]
|
||||
for basecls in inspect.getmro(self.obj.__class__):
|
||||
dicts.append(basecls.__dict__)
|
||||
|
@ -254,9 +255,6 @@ class Instance(PyCollectorMixin, pytest.collect.Collector):
|
|||
def _getobj(self):
|
||||
return self.parent.obj()
|
||||
|
||||
def _keywords(self):
|
||||
return []
|
||||
|
||||
def newinstance(self):
|
||||
self.obj = self._getobj()
|
||||
return self.obj
|
||||
|
@ -449,6 +447,7 @@ def hasinit(obj):
|
|||
|
||||
|
||||
def getfuncargnames(function):
|
||||
# XXX merge with _core.py's varnames
|
||||
argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0]
|
||||
startindex = py.std.inspect.ismethod(function) and 1 or 0
|
||||
defaults = getattr(function, 'func_defaults',
|
||||
|
|
|
@ -249,7 +249,7 @@ class CollectReport(BaseReport):
|
|||
self.fspath = fspath
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result
|
||||
self.result = result or []
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
|
|
|
@ -266,7 +266,7 @@ class Collection:
|
|||
node.ihook.pytest_collectstart(collector=node)
|
||||
rep = node.ihook.pytest_make_collect_report(collector=node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result or []:
|
||||
for subnode in rep.result:
|
||||
for x in self.genitems(subnode):
|
||||
yield x
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
|
@ -328,7 +328,7 @@ class Node(object):
|
|||
#: the file where this item is contained/collected from.
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
self.ihook = HookProxy(self)
|
||||
self.keywords = self.readkeywords()
|
||||
self.keywords = {self.name: True}
|
||||
|
||||
def __repr__(self):
|
||||
if getattr(self.config.option, 'debug', False):
|
||||
|
@ -344,7 +344,7 @@ class Node(object):
|
|||
if not isinstance(other, Node):
|
||||
return False
|
||||
return self.__class__ == other.__class__ and \
|
||||
self.name == other.name and self.parent == other.parent
|
||||
self.name == other.name and self.parent == other.parent
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
@ -396,12 +396,6 @@ class Node(object):
|
|||
current = current.parent
|
||||
return current
|
||||
|
||||
def readkeywords(self):
|
||||
return dict([(x, True) for x in self._keywords()])
|
||||
|
||||
def _keywords(self):
|
||||
return [self.name]
|
||||
|
||||
def _prunetraceback(self, traceback):
|
||||
return traceback
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -22,7 +22,7 @@ def main():
|
|||
name='pytest',
|
||||
description='py.test: simple powerful testing with Python',
|
||||
long_description = long_description,
|
||||
version='2.0.0.dev7',
|
||||
version='2.0.0.dev8',
|
||||
url='http://pytest.org',
|
||||
license='MIT license',
|
||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||
|
|
|
@ -8,13 +8,15 @@ class TestMark:
|
|||
|
||||
def test_pytest_mark_bare(self):
|
||||
mark = Mark()
|
||||
def f(): pass
|
||||
def f():
|
||||
pass
|
||||
mark.hello(f)
|
||||
assert f.hello
|
||||
|
||||
def test_pytest_mark_keywords(self):
|
||||
mark = Mark()
|
||||
def f(): pass
|
||||
def f():
|
||||
pass
|
||||
mark.world(x=3, y=4)(f)
|
||||
assert f.world
|
||||
assert f.world.kwargs['x'] == 3
|
||||
|
@ -22,7 +24,8 @@ class TestMark:
|
|||
|
||||
def test_apply_multiple_and_merge(self):
|
||||
mark = Mark()
|
||||
def f(): pass
|
||||
def f():
|
||||
pass
|
||||
marker = mark.world
|
||||
mark.world(x=3)(f)
|
||||
assert f.world.kwargs['x'] == 3
|
||||
|
@ -35,7 +38,8 @@ class TestMark:
|
|||
|
||||
def test_pytest_mark_positional(self):
|
||||
mark = Mark()
|
||||
def f(): pass
|
||||
def f():
|
||||
pass
|
||||
mark.world("hello")(f)
|
||||
assert f.world.args[0] == "hello"
|
||||
mark.world("world")(f)
|
||||
|
@ -62,7 +66,7 @@ class TestFunctional:
|
|||
assert 'hello' in keywords
|
||||
|
||||
def test_marklist_per_class(self, testdir):
|
||||
modcol = testdir.getmodulecol("""
|
||||
item = testdir.getitem("""
|
||||
import py
|
||||
class TestClass:
|
||||
pytestmark = [py.test.mark.hello, py.test.mark.world]
|
||||
|
@ -70,13 +74,11 @@ class TestFunctional:
|
|||
assert TestClass.test_func.hello
|
||||
assert TestClass.test_func.world
|
||||
""")
|
||||
clscol = modcol.collect()[0]
|
||||
item = clscol.collect()[0].collect()[0]
|
||||
keywords = item.keywords
|
||||
assert 'hello' in keywords
|
||||
|
||||
def test_marklist_per_module(self, testdir):
|
||||
modcol = testdir.getmodulecol("""
|
||||
item = testdir.getitem("""
|
||||
import py
|
||||
pytestmark = [py.test.mark.hello, py.test.mark.world]
|
||||
class TestClass:
|
||||
|
@ -84,29 +86,25 @@ class TestFunctional:
|
|||
assert TestClass.test_func.hello
|
||||
assert TestClass.test_func.world
|
||||
""")
|
||||
clscol = modcol.collect()[0]
|
||||
item = clscol.collect()[0].collect()[0]
|
||||
keywords = item.keywords
|
||||
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("""
|
||||
item = testdir.getitem("""
|
||||
import py
|
||||
@py.test.mark.hello
|
||||
class TestClass:
|
||||
def test_func(self):
|
||||
assert TestClass.test_func.hello
|
||||
""")
|
||||
clscol = modcol.collect()[0]
|
||||
item = clscol.collect()[0].collect()[0]
|
||||
keywords = item.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("""
|
||||
item = testdir.getitem("""
|
||||
import py
|
||||
@py.test.mark.hello
|
||||
class TestClass:
|
||||
|
@ -115,8 +113,6 @@ class TestFunctional:
|
|||
assert TestClass.test_func.hello
|
||||
assert TestClass.test_func.world
|
||||
""")
|
||||
clscol = modcol.collect()[0]
|
||||
item = clscol.collect()[0].collect()[0]
|
||||
keywords = item.keywords
|
||||
assert 'hello' in keywords
|
||||
assert 'world' in keywords
|
||||
|
@ -140,14 +136,15 @@ class TestFunctional:
|
|||
assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4}
|
||||
|
||||
def test_mark_other(self, testdir):
|
||||
item = testdir.getitem("""
|
||||
import py
|
||||
class pytestmark:
|
||||
pass
|
||||
def test_func():
|
||||
pass
|
||||
""")
|
||||
keywords = item.keywords
|
||||
py.test.raises(TypeError, '''
|
||||
testdir.getitem("""
|
||||
import py
|
||||
class pytestmark:
|
||||
pass
|
||||
def test_func():
|
||||
pass
|
||||
""")
|
||||
''')
|
||||
|
||||
def test_mark_dynamically_in_funcarg(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
|
@ -223,7 +220,8 @@ class Test_genitems:
|
|||
class TestKeywordSelection:
|
||||
def test_select_simple(self, testdir):
|
||||
file_test = testdir.makepyfile("""
|
||||
def test_one(): assert 0
|
||||
def test_one():
|
||||
assert 0
|
||||
class TestClass(object):
|
||||
def test_method_one(self):
|
||||
assert 42 == 43
|
||||
|
|
|
@ -70,3 +70,14 @@ def test_teardown(testdir):
|
|||
assert passed == 2
|
||||
assert passed + skipped + failed == 2
|
||||
|
||||
def test_module_level_pytestmark(testdir):
|
||||
testpath = testdir.makepyfile("""
|
||||
import unittest
|
||||
import py
|
||||
pytestmark = py.test.mark.xfail
|
||||
class MyTestCase(unittest.TestCase):
|
||||
def test_func1(self):
|
||||
assert 0
|
||||
""")
|
||||
reprec = testdir.inline_run(testpath, "-s")
|
||||
reprec.assertoutcome(skipped=1)
|
||||
|
|
Loading…
Reference in New Issue