From 44804011190bb8eb063b3cb5af972cf3450f24f1 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 25 Oct 2010 23:08:56 +0200 Subject: [PATCH] allow unittest test functions to work with the "pytestmark" mechanism by refactoring mark/keyword handling and initialization --HG-- branch : trunk --- CHANGELOG | 2 + pytest/__init__.py | 2 +- pytest/_core.py | 6 +-- pytest/plugin/mark.py | 81 +++++++++++++++++---------------- pytest/plugin/pytester.py | 7 +-- pytest/plugin/python.py | 5 +- pytest/plugin/runner.py | 2 +- pytest/plugin/session.py | 12 ++--- setup.py | 2 +- testing/plugin/test_mark.py | 48 ++++++++++--------- testing/plugin/test_unittest.py | 11 +++++ 11 files changed, 90 insertions(+), 88 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ebd052a4c..76121a175 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 ---------------------------------------------- diff --git a/pytest/__init__.py b/pytest/__init__.py index 0f4409e9e..b6e4db4df 100644 --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -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'] diff --git a/pytest/_core.py b/pytest/_core.py index 2ce7ea9f6..d28ab2643 100644 --- a/pytest/_core.py +++ b/pytest/_core.py @@ -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) diff --git a/pytest/plugin/mark.py b/pytest/plugin/mark.py index 07172c465..17317671e 100644 --- a/pytest/plugin/mark.py +++ b/pytest/plugin/mark.py @@ -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 "" % ( 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)) diff --git a/pytest/plugin/pytester.py b/pytest/plugin/pytester.py index 5ab568152..b5ed3d739 100644 --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -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) diff --git a/pytest/plugin/python.py b/pytest/plugin/python.py index 8a53cf5a9..d2ff143de 100644 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -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', diff --git a/pytest/plugin/runner.py b/pytest/plugin/runner.py index ca7b145a9..cc3b9a9f2 100644 --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -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 diff --git a/pytest/plugin/session.py b/pytest/plugin/session.py index cacdf5533..388220215 100644 --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -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 diff --git a/setup.py b/setup.py index f3eb399f0..f24fa28c1 100644 --- a/setup.py +++ b/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'], diff --git a/testing/plugin/test_mark.py b/testing/plugin/test_mark.py index f3a26c8b5..96f5536c5 100644 --- a/testing/plugin/test_mark.py +++ b/testing/plugin/test_mark.py @@ -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 diff --git a/testing/plugin/test_unittest.py b/testing/plugin/test_unittest.py index 4c3c13ec0..40ae56556 100644 --- a/testing/plugin/test_unittest.py +++ b/testing/plugin/test_unittest.py @@ -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)