From 95bafbccd1a8d120d8db55edb475b6ad4745e4b7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 4 Sep 2010 09:21:35 +0200 Subject: [PATCH 01/38] fix issue116 : --doctestmodules also works in the presence of __init__.py files, done by fixing the underlyingly used path.pyimport() --HG-- branch : trunk --- CHANGELOG | 5 +++++ py/_path/local.py | 5 ++++- testing/path/test_local.py | 4 +++- testing/plugin/test_pytest_doctest.py | 9 +++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5c66a8772..e39490049 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +Changes between 1.3.3 and XXX +================================================== + +- fix issue116: --doctestmodules works in the presence of __init__.py files as well + Changes between 1.3.2 and 1.3.3 ================================================== diff --git a/py/_path/local.py b/py/_path/local.py index 0e051eb8d..1765b80ff 100644 --- a/py/_path/local.py +++ b/py/_path/local.py @@ -519,6 +519,8 @@ class LocalPath(FSBase): pkg = __import__(pkgpath.basename, None, None, []) names = self.new(ext='').relto(pkgpath.dirpath()) names = names.split(self.sep) + if names and names[-1] == "__init__": + names.pop() modname = ".".join(names) else: # no package scope, still make it possible @@ -532,7 +534,8 @@ class LocalPath(FSBase): elif modfile.endswith('$py.class'): modfile = modfile[:-9] + '.py' if modfile.endswith("__init__.py"): - modfile = modfile[:-12] + if self.basename != "__init__.py": + modfile = modfile[:-12] if not self.samefile(modfile): raise self.ImportMismatchError(modname, modfile, self) return mod diff --git a/testing/path/test_local.py b/testing/path/test_local.py index e13a4899c..5b296288b 100644 --- a/testing/path/test_local.py +++ b/testing/path/test_local.py @@ -306,9 +306,11 @@ class TestImport: def test_pyimport_dir(self, tmpdir): p = tmpdir.join("hello_123") - p.ensure("__init__.py") + p_init = p.ensure("__init__.py") m = p.pyimport() assert m.__name__ == "hello_123" + m = p_init.pyimport() + assert m.__name__ == "hello_123" def test_pyimport_execfile_different_name(self, path1): obj = path1.join('execfile.py').pyimport(modname="0x.y.z") diff --git a/testing/plugin/test_pytest_doctest.py b/testing/plugin/test_pytest_doctest.py index 161ddf44f..ae13ef69b 100644 --- a/testing/plugin/test_pytest_doctest.py +++ b/testing/plugin/test_pytest_doctest.py @@ -1,4 +1,5 @@ from py._plugin.pytest_doctest import DoctestModule, DoctestTextfile +import py pytest_plugins = ["pytest_doctest"] @@ -73,16 +74,16 @@ class TestDoctests: reprec = testdir.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) - def test_doctestmodule_external(self, testdir): - p = testdir.makepyfile(""" - # + def test_doctestmodule_external_and_issue116(self, testdir): + p = testdir.mkpydir("hello") + p.join("__init__.py").write(py.code.Source(""" def somefunc(): ''' >>> i = 0 >>> i + 1 2 ''' - """) + """)) result = testdir.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines([ '004 *>>> i = 0', From cd013746cfa054f5115c8a4b25293b102a17b230 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Sep 2010 19:35:17 +0100 Subject: [PATCH 02/38] Initial patch as sent to py-dev With a small but disasterous typo fixed though. --HG-- branch : trunk --- py/_code/_assertionnew.py | 14 +++--- py/_code/assertion.py | 19 ++++++-- py/_plugin/hookspec.py | 13 ++++++ py/_plugin/pytest_assertion.py | 49 ++++++++++++++++++++ testing/code/test_assertionnew.py | 74 +++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 testing/code/test_assertionnew.py diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 0c7b0090e..a80ae898c 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -162,10 +162,7 @@ class DebugInterpreter(ast.NodeVisitor): def visit_Compare(self, comp): left = comp.left left_explanation, left_result = self.visit(left) - got_result = False for op, next_op in zip(comp.ops, comp.comparators): - if got_result and not result: - break next_explanation, next_result = self.visit(next_op) op_symbol = operator_map[op.__class__] explanation = "%s %s %s" % (left_explanation, op_symbol, @@ -177,9 +174,16 @@ class DebugInterpreter(ast.NodeVisitor): __exprinfo_right=next_result) except Exception: raise Failure(explanation) - else: - got_result = True + if not result: + break left_explanation, left_result = next_explanation, next_result + hook_result = py.test.config.hook.pytest_assert_compare( + op=op_symbol, left=left_result, right=next_result) + if hook_result: + for new_expl in hook_result: + if new_expl: + explanation = '\n~'.join(new_expl) + break return explanation, result def visit_BoolOp(self, boolop): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 2a2da9cfb..558ee740e 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -5,12 +5,20 @@ BuiltinAssertionError = py.builtin.builtins.AssertionError def _format_explanation(explanation): - # uck! See CallFunc for where \n{ and \n} escape sequences are used + """This formats an explanation + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ raw_lines = (explanation or '').split('\n') - # escape newlines not followed by { and } + # escape newlines not followed by {, } and ~ lines = [raw_lines[0]] for l in raw_lines[1:]: - if l.startswith('{') or l.startswith('}'): + if l.startswith('{') or l.startswith('}') or l.startswith('~'): lines.append(l) else: lines[-1] += '\\n' + l @@ -28,11 +36,14 @@ def _format_explanation(explanation): stackcnt[-1] += 1 stackcnt.append(0) result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) - else: + elif line.startswith('}'): assert line.startswith('}') stack.pop() stackcnt.pop() result[stack[-1]] += line[1:] + else: + assert line.startswith('~') + result.append(' '*len(stack) + line[1:]) assert len(stack) == 1 return '\n'.join(result) diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 04e1bd0d2..f295211cd 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -123,6 +123,19 @@ def pytest_sessionstart(session): def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ +# ------------------------------------------------------------------------- +# hooks for customising the assert methods +# ------------------------------------------------------------------------- + +def pytest_assert_compare(op, left, right): + """Customise compare assertion + + Return None or an empty list for no custom compare, otherwise + return a list of strings. The strings will be joined by newlines + but any newlines *in* as string will be escaped. Note that all + but the first line will be indented sligthly. + """ + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index f18350e7c..5e9c2b81b 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -1,3 +1,6 @@ +import difflib +import pprint + import py import sys @@ -26,3 +29,49 @@ def warn_about_missing_assertion(): else: py.std.warnings.warn("Assertions are turned off!" " (are you using python -O?)") + + +def pytest_assert_compare(op, left, right): + """Make a specialised explanation for comapare equal""" + if op != '==' or type(left) != type(right): + return None + explanation = [] + left_repr = py.io.saferepr(left, maxsize=30) + right_repr = py.io.saferepr(right, maxsize=30) + explanation += ['%s == %s' % (left_repr, right_repr)] + issquence = lambda x: isinstance(x, (list, tuple)) + istext = lambda x: isinstance(x, basestring) + isdict = lambda x: isinstance(x, dict) + if istext(left): + explanation += [line.strip('\n') for line in + difflib.ndiff(left.splitlines(), right.splitlines())] + elif issquence(left): + explanation += _compare_eq_sequence(left, right) + elif isdict(left): + explanation += _pprint_diff(left, right) + else: + return None # No specialised knowledge + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in xrange(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['First differing item %s: %s != %s' % + (i, left[i], right[i])] + break + if len(left) > len(right): + explanation += ['Left contains more items, ' + 'first extra item: %s' % left[len(right)]] + elif len(left) < len(right): + explanation += ['Right contains more items, ' + 'first extra item: %s' % right[len(right)]] + return explanation + _pprint_diff(left, right) + + +def _pprint_diff(left, right): + """Make explanation using pprint and difflib""" + return [line.strip('\n') for line in + difflib.ndiff(pprint.pformat(left).splitlines(), + pprint.pformat(right).splitlines())] diff --git a/testing/code/test_assertionnew.py b/testing/code/test_assertionnew.py new file mode 100644 index 000000000..ebc0edf77 --- /dev/null +++ b/testing/code/test_assertionnew.py @@ -0,0 +1,74 @@ +import sys + +import py +from py._code._assertionnew import interpret + + +def getframe(): + """Return the frame of the caller as a py.code.Frame object""" + return py.code.Frame(sys._getframe(1)) + + +def setup_module(mod): + py.code.patch_builtins(assertion=True, compile=False) + + +def teardown_module(mod): + py.code.unpatch_builtins(assertion=True, compile=False) + + +def test_assert_simple(): + # Simply test that this way of testing works + a = 0 + b = 1 + r = interpret('assert a == b', getframe()) + assert r == 'assert 0 == 1' + + +def test_assert_list(): + r = interpret('assert [0, 1] == [0, 2]', getframe()) + msg = ('assert [0, 1] == [0, 2]\n' + ' First differing item 1: 1 != 2\n' + ' - [0, 1]\n' + ' ? ^\n' + ' + [0, 2]\n' + ' ? ^') + print r + assert r == msg + + +def test_assert_string(): + r = interpret('assert "foo and bar" == "foo or bar"', getframe()) + msg = ("assert 'foo and bar' == 'foo or bar'\n" + " - foo and bar\n" + " ? ^^^\n" + " + foo or bar\n" + " ? ^^") + print r + assert r == msg + + +def test_assert_multiline_string(): + a = 'foo\nand bar\nbaz' + b = 'foo\nor bar\nbaz' + r = interpret('assert a == b', getframe()) + msg = ("assert 'foo\\nand bar\\nbaz' == 'foo\\nor bar\\nbaz'\n" + ' foo\n' + ' - and bar\n' + ' + or bar\n' + ' baz') + print r + assert r == msg + + +def test_assert_dict(): + a = {'a': 0, 'b': 1} + b = {'a': 0, 'c': 2} + r = interpret('assert a == b', getframe()) + msg = ("assert {'a': 0, 'b': 1} == {'a': 0, 'c': 2}\n" + " - {'a': 0, 'b': 1}\n" + " ? ^ ^\n" + " + {'a': 0, 'c': 2}\n" + " ? ^ ^") + print r + assert r == msg From f194b16a09dfaca18fdf50061060efd28a4f4538 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Sep 2010 19:46:58 +0100 Subject: [PATCH 03/38] Don't import difflib and pprint up-front Builtin plugins need to keep their import time to a minimum. Therefore it's better to delay importing till you really need it, i.e. use py.std.* in this case. --HG-- branch : trunk --- py/_plugin/pytest_assertion.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 5e9c2b81b..1e5452f07 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -1,6 +1,3 @@ -import difflib -import pprint - import py import sys @@ -44,7 +41,7 @@ def pytest_assert_compare(op, left, right): isdict = lambda x: isinstance(x, dict) if istext(left): explanation += [line.strip('\n') for line in - difflib.ndiff(left.splitlines(), right.splitlines())] + py.std.difflib.ndiff(left.splitlines(), right.splitlines())] elif issquence(left): explanation += _compare_eq_sequence(left, right) elif isdict(left): @@ -73,5 +70,5 @@ def _compare_eq_sequence(left, right): def _pprint_diff(left, right): """Make explanation using pprint and difflib""" return [line.strip('\n') for line in - difflib.ndiff(pprint.pformat(left).splitlines(), - pprint.pformat(right).splitlines())] + py.std.difflib.ndiff(py.std.pprint.pformat(left).splitlines(), + py.std.pprint.pformat(right).splitlines())] From c17bb32f70fb9f05dcfdb2c7072edc956b01b3f6 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 Sep 2010 10:03:11 +0200 Subject: [PATCH 04/38] patch from flub to allow callable objects as hook implementations --HG-- branch : trunk --- py/_test/pluginmanager.py | 2 ++ testing/test_pluginmanager.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index e6a0895ce..629498e26 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -259,6 +259,8 @@ class MultiCall: return kwargs def varnames(func): + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) ismethod = inspect.ismethod(func) rawcode = py.code.getrawcode(func) try: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index ee919d95c..8b705d7b3 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -340,8 +340,12 @@ def test_varnames(): class A: def f(self, y): pass + class B(object): + def __call__(self, z): + pass assert varnames(f) == ("x",) assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) class TestMultiCall: def test_uses_copy_of_methods(self): From 2b59200786441646a99ff7dfc1a7c5251e305bdf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 8 Sep 2010 12:00:36 +0200 Subject: [PATCH 05/38] implement and naively test the native traceback style --HG-- branch : trunk --- py/_code/code.py | 10 +++++++++- py/_plugin/pytest_terminal.py | 2 +- testing/code/test_excinfo.py | 9 +++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/py/_code/code.py b/py/_code/code.py index 4e8842b61..6558a8dcf 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -354,9 +354,17 @@ class ExceptionInfo(object): abspath=False, tbfilter=True, funcargs=False): """ return str()able representation of this exception info. showlocals: show locals per traceback entry - style: long|short|no traceback style + style: long|short|no|native traceback style tbfilter: hide entries (where __tracebackhide__ is true) """ + if style == 'native': + import traceback + return ''.join(traceback.format_exception( + self.type, + self.value, + self.traceback[0]._rawentry, + )) + fmt = FormattedExcinfo(showlocals=showlocals, style=style, abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) return fmt.repr_excinfo(self) diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index be3d21e5f..6487c0662 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -22,7 +22,7 @@ def pytest_addoption(parser): help="(deprecated, use -r)") group._addoption('--tb', metavar="style", action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no', 'line'], + type="choice", choices=['long', 'short', 'no', 'line', 'native'], help="traceback print mode (long/short/line/no).") group._addoption('--fulltrace', action="store_true", dest="fulltrace", default=False, diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index e8db92c83..a596d8dfe 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -700,3 +700,12 @@ raise ValueError() repr = excinfo.getrepr(**reproptions) repr.toterminal(tw) assert tw.stringio.getvalue() + + + def test_native_style(self): + excinfo = self.excinfo_from_exec(""" + assert 0 + """) + repr = excinfo.getrepr(style='native') + assert repr.startswith('Traceback (most recent call last):\n File') + assert repr.endswith('\n assert 0\nAssertionError: assert 0\n') From 7903fbb8ce2bd9443939ba8599054cf3b26f701a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 8 Sep 2010 16:51:46 +0200 Subject: [PATCH 06/38] applied ronny's patch, fixes #116 --HG-- branch : trunk --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index e39490049..7d4d66392 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Changes between 1.3.3 and XXX ================================================== - fix issue116: --doctestmodules works in the presence of __init__.py files as well +- fix issue118: new --tb=native option for presenting cpython-standard exceptions Changes between 1.3.2 and 1.3.3 ================================================== From 6f40441ef854d0ae31f8bebff9f59898afcd282f Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 8 Sep 2010 18:29:26 +0200 Subject: [PATCH 07/38] fixing test for python2.4 (thanks ronny) --HG-- branch : trunk --- testing/code/test_excinfo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index a596d8dfe..e158944c9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -708,4 +708,8 @@ raise ValueError() """) repr = excinfo.getrepr(style='native') assert repr.startswith('Traceback (most recent call last):\n File') - assert repr.endswith('\n assert 0\nAssertionError: assert 0\n') + assert repr.endswith('\nAssertionError: assert 0\n') + assert 'exec (source.compile())' in repr + # python 2.4 fails to get the source line for the assert + if py.std.sys.version_info >= (2, 5): + assert repr.count('assert 0') == 2 From 6fb56443a9a1d7a5424f04f53b97ae3611a48bf9 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Sep 2010 22:21:52 +0100 Subject: [PATCH 08/38] Split the tests between the core and plugin The tests for _assertionnew are much better, the ones for pytest_assert_compare() are still not great. --HG-- branch : trunk --- testing/code/test_assertionnew.py | 88 ++++++++----------------- testing/plugin/test_pytest_assertion.py | 32 +++++++++ 2 files changed, 61 insertions(+), 59 deletions(-) diff --git a/testing/code/test_assertionnew.py b/testing/code/test_assertionnew.py index ebc0edf77..8511f8125 100644 --- a/testing/code/test_assertionnew.py +++ b/testing/code/test_assertionnew.py @@ -9,66 +9,36 @@ def getframe(): return py.code.Frame(sys._getframe(1)) -def setup_module(mod): - py.code.patch_builtins(assertion=True, compile=False) +def pytest_funcarg__hook(request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() + + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + return MockHook() -def teardown_module(mod): - py.code.unpatch_builtins(assertion=True, compile=False) +def test_pytest_assert_compare_called(monkeypatch, hook): + monkeypatch.setattr(py._plugin.pytest_assertion, + 'pytest_assert_compare', hook) + interpret('assert 0 == 1', getframe()) + assert hook.called -def test_assert_simple(): - # Simply test that this way of testing works - a = 0 - b = 1 - r = interpret('assert a == b', getframe()) - assert r == 'assert 0 == 1' - - -def test_assert_list(): - r = interpret('assert [0, 1] == [0, 2]', getframe()) - msg = ('assert [0, 1] == [0, 2]\n' - ' First differing item 1: 1 != 2\n' - ' - [0, 1]\n' - ' ? ^\n' - ' + [0, 2]\n' - ' ? ^') - print r - assert r == msg - - -def test_assert_string(): - r = interpret('assert "foo and bar" == "foo or bar"', getframe()) - msg = ("assert 'foo and bar' == 'foo or bar'\n" - " - foo and bar\n" - " ? ^^^\n" - " + foo or bar\n" - " ? ^^") - print r - assert r == msg - - -def test_assert_multiline_string(): - a = 'foo\nand bar\nbaz' - b = 'foo\nor bar\nbaz' - r = interpret('assert a == b', getframe()) - msg = ("assert 'foo\\nand bar\\nbaz' == 'foo\\nor bar\\nbaz'\n" - ' foo\n' - ' - and bar\n' - ' + or bar\n' - ' baz') - print r - assert r == msg - - -def test_assert_dict(): - a = {'a': 0, 'b': 1} - b = {'a': 0, 'c': 2} - r = interpret('assert a == b', getframe()) - msg = ("assert {'a': 0, 'b': 1} == {'a': 0, 'c': 2}\n" - " - {'a': 0, 'b': 1}\n" - " ? ^ ^\n" - " + {'a': 0, 'c': 2}\n" - " ? ^ ^") - print r - assert r == msg +def test_pytest_assert_compare_args(monkeypatch, hook): + print hook.called + monkeypatch.setattr(py._plugin.pytest_assertion, + 'pytest_assert_compare', hook) + interpret('assert [0, 1] == [0, 2]', getframe()) + print hook.called + print hook.left + print hook.right + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index c76a93b4d..4c34a0797 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -1,3 +1,7 @@ +import py +import py._plugin.pytest_assertion as plugin + + def test_functional(testdir): testdir.makepyfile(""" def test_hello(): @@ -49,3 +53,31 @@ def test_traceback_failure(testdir): "*test_traceback_failure.py:4: AssertionError" ]) + +class Test_pytest_assert_compare: + def test_different_types(self): + assert plugin.pytest_assert_compare('==', [0, 1], 'foo') is None + + def test_summary(self): + summary = plugin.pytest_assert_compare('==', [0, 1], [0, 2])[0] + assert len(summary) < 65 + + def test_text_diff(self): + diff = plugin.pytest_assert_compare('==', 'spam', 'eggs')[1:] + assert '- spam' in diff + assert '+ eggs' in diff + + def test_multiline_text_diff(self): + left = 'foo\nspam\nbar' + right = 'foo\neggs\nbar' + diff = plugin.pytest_assert_compare('==', left, right) + assert '- spam' in diff + assert '+ eggs' in diff + + def test_list(self): + expl = plugin.pytest_assert_compare('==', [0, 1], [0, 2]) + assert len(expl) > 1 + + def test_dict(self): + expl = plugin.pytest_assert_compare('==', {'a': 0}, {'a': 1}) + assert len(expl) > 1 From af412d993c62733cb61e0ba8c91a96b19baea1ea Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 15:43:00 +0200 Subject: [PATCH 09/38] simplify and fix installation instructions particularly for windows (fixes #111) and bump version to 1.3.4 --HG-- branch : trunk --- CHANGELOG | 3 +- doc/install.txt | 86 ++++++++++++++++++++++++------------------------- py/__init__.py | 2 +- setup.py | 4 +-- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d4d66392..10ce4397f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ -Changes between 1.3.3 and XXX +Changes between 1.3.3 and 1.3.4 ================================================== +- fix issue111: improve install documentation for windows - fix issue116: --doctestmodules works in the presence of __init__.py files as well - fix issue118: new --tb=native option for presenting cpython-standard exceptions diff --git a/doc/install.txt b/doc/install.txt index 64d9b0c6f..43d37b1bf 100644 --- a/doc/install.txt +++ b/doc/install.txt @@ -26,7 +26,47 @@ py.test/pylib installation info in a nutshell .. _`bin`: bin.html -Best practise: install tool and dependencies virtually +.. _`easy_install`: + +Installation using easy_install +=================================================== + +Both `Distribute`_ and setuptools_ provide the ``easy_install`` +installation tool with which you can type into a command line window:: + + easy_install -U py + +to install the latest release of the py lib and py.test. The ``-U`` switch +will trigger an upgrade if you already have an older version installed. +Note that setuptools works ok with Python2 interpreters while `Distribute`_ +additionally works with Python3 and also avoid some issues on Windows. + +Known issues: + +- **Windows**: If "easy_install" or "py.test" are not found + please see here for preparing your environment for running + command line tools: `Python for Windows`_. You may alternatively + use an `ActivePython install`_ which makes command line tools + automatically available under Windows. + +.. _`ActivePython install`: http://www.activestate.com/activepython/downloads + +.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 + +- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ + so ``py.test`` will not work correctly. You may install py.test on + CPython and type ``py.test --genscript=mytest`` and then use + ``jython mytest`` to run py.test for your tests to run in Jython. + +- **On Linux**: If ``easy_install`` fails because it needs to run + as the superuser you are trying to install things globally + and need to put ``sudo`` in front of the command. + + +.. _quickstart: test/quickstart.html + + +Recommendation: install tool and dependencies virtually =========================================================== It is recommended to work with virtual environments @@ -36,34 +76,9 @@ you need to run your tests. Local virtual Python environments (as opposed to system-wide "global" environments) make for a more reproducible and reliable test environment. - .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip -.. _`easy_install`: - -using easy_install (from setuptools or Distribute) -=================================================== - -Both `Distribute`_ and setuptools_ provide the ``easy_install`` -installation tool. While setuptools should work ok with -Python2 interpreters, `Distribute`_ also works with Python3 -and it avoids some issues on Windows. In both cases you -can open a command line window and then type:: - - easy_install -U py - -to install the latest release of the py lib and py.test. The ``-U`` switch -will trigger an upgrade if you already have an older version installed. - -If you now type:: - - py.test --version - -you should see the version number and the import location of the tool. -Maybe you want to head on with the `quickstart`_ now? - -.. _quickstart: test/quickstart.html .. _standalone: @@ -84,24 +99,7 @@ disguise. You can tell people to download and then e.g. run it like this:: and ask them to send you the resulting URL. The resulting script has all core features and runs unchanged under Python2 and Python3 interpreters. -Troubleshooting / known issues -=============================== - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -**Jython2.5.1 on XP**: `Jython does not create command line launchers`_ -so ``py.test`` will not work correctly. You may install py.test on -CPython and type ``py.test --genscript=mytest`` and then use -``jython mytest`` to run py.test for your tests to run in Jython. - -**On Linux**: If ``easy_install`` fails because it needs to run -as the superuser you are trying to install things globally -and need to put ``sudo`` in front of the command. - -**On Windows**: If "easy_install" or "py.test" are not found -please see here: `How do i run a Python program under Windows?`_ - -.. _`How do i run a Python program under Windows?`: http://www.python.org/doc/faq/windows/#how-do-i-run-a-python-program-under-windows +.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html .. _mercurial: http://mercurial.selenic.com/wiki/ .. _`Distribute`: diff --git a/py/__init__.py b/py/__init__.py index 32fa85b41..dceedad4b 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = version = "1.3.4a1" +__version__ = version = "1.3.4" import py.apipkg diff --git a/setup.py b/setup.py index cf93eeb6a..74d41fb3a 100644 --- a/setup.py +++ b/setup.py @@ -26,14 +26,14 @@ def main(): name='py', description='py.test and pylib: rapid testing and development utils.', long_description = long_description, - version= '1.3.4a1', + version= '1.3.4', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - classifiers=['Development Status :: 5 - Production/Stable', + classifiers=['Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', From 04b3b9a3dafae943c87ef450070f9bf1b97ec8c8 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 15:46:30 +0200 Subject: [PATCH 10/38] preliminary release announcement --HG-- branch : trunk --- doc/announce/release-1.3.4.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 doc/announce/release-1.3.4.txt diff --git a/doc/announce/release-1.3.4.txt b/doc/announce/release-1.3.4.txt new file mode 100644 index 000000000..867a412ff --- /dev/null +++ b/doc/announce/release-1.3.4.txt @@ -0,0 +1,18 @@ +py.test/pylib 1.3.4: fixes and new native traceback option +=========================================================================== + +pylib/py.test 1.3.4 is a minor bugfix release featuring small fixes. +See changelog_ for full history. + +have fun, +holger krekel + +.. _changelog: ../changelog.html + +Changes between 1.3.3 and 1.3.4 +================================================== + +- fix issue111: improve install documentation for windows +- fix issue116: --doctestmodules works in the presence of __init__.py files as well +- fix issue118: new --tb=native option for presenting cpython-standard exceptions + From b81e48507c7843551c9494dd9df1528bc09dbb08 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 14 Sep 2010 16:12:50 +0200 Subject: [PATCH 11/38] introduce py.builtin._sysex as alias for the special exceptions, fixes #115 --HG-- branch : trunk --- CHANGELOG | 1 + py/__init__.py | 1 + py/_builtin.py | 2 ++ py/_code/_assertionold.py | 2 +- py/_code/assertion.py | 2 +- py/_code/code.py | 2 +- py/_code/source.py | 2 +- py/_io/saferepr.py | 4 +--- py/_io/terminalwriter.py | 2 +- py/_test/collect.py | 2 +- 10 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 10ce4397f..bf20775f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ Changes between 1.3.3 and 1.3.4 - fix issue111: improve install documentation for windows - fix issue116: --doctestmodules works in the presence of __init__.py files as well - fix issue118: new --tb=native option for presenting cpython-standard exceptions +- fix issue115: introduce py.builtin._sysex for system level exceptions we should thread different Changes between 1.3.2 and 1.3.3 ================================================== diff --git a/py/__init__.py b/py/__init__.py index dceedad4b..c6e95d21d 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -111,6 +111,7 @@ py.apipkg.initpkg(__name__, dict( 'frozenset' : '._builtin:frozenset', 'BaseException' : '._builtin:BaseException', 'GeneratorExit' : '._builtin:GeneratorExit', + '_sysex' : '._builtin:_sysex', 'print_' : '._builtin:print_', '_reraise' : '._builtin:_reraise', '_tryimport' : '._builtin:_tryimport', diff --git a/py/_builtin.py b/py/_builtin.py index 8b7ca1421..a356db044 100644 --- a/py/_builtin.py +++ b/py/_builtin.py @@ -87,6 +87,8 @@ except NameError: pass GeneratorExit.__module__ = 'exceptions' +_sysex = (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit) + if sys.version_info >= (3, 0): exec ("print_ = print ; exec_=exec") import builtins diff --git a/py/_code/_assertionold.py b/py/_code/_assertionold.py index 6df1d55d3..4e81fb3ef 100644 --- a/py/_code/_assertionold.py +++ b/py/_code/_assertionold.py @@ -3,7 +3,7 @@ import sys, inspect from compiler import parse, ast, pycodegen from py._code.assertion import BuiltinAssertionError, _format_explanation -passthroughex = (KeyboardInterrupt, SystemExit, MemoryError) +passthroughex = py.builtin._sysex class Failure: def __init__(self, node): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 2a2da9cfb..efedec63d 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -44,7 +44,7 @@ class AssertionError(BuiltinAssertionError): if args: try: self.msg = str(args[0]) - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: self.msg = "<[broken __repr__] %s at %0xd>" %( diff --git a/py/_code/code.py b/py/_code/code.py index 6558a8dcf..b40ef28c4 100644 --- a/py/_code/code.py +++ b/py/_code/code.py @@ -189,7 +189,7 @@ class TracebackEntry(object): """ try: return self.frame.eval("__tracebackhide__") - except (SystemExit, KeyboardInterrupt): + except py.builtin._sysex: raise except: return False diff --git a/py/_code/source.py b/py/_code/source.py index 52ab252a0..be2ca55d6 100644 --- a/py/_code/source.py +++ b/py/_code/source.py @@ -276,7 +276,7 @@ def getfslineno(obj): def findsource(obj): try: sourcelines, lineno = py.std.inspect.findsource(obj) - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: return None, None diff --git a/py/_io/saferepr.py b/py/_io/saferepr.py index db52ca92b..afc968d3a 100644 --- a/py/_io/saferepr.py +++ b/py/_io/saferepr.py @@ -5,8 +5,6 @@ builtin_repr = repr reprlib = py.builtin._tryimport('repr', 'reprlib') -sysex = (KeyboardInterrupt, MemoryError, SystemExit) - class SafeRepr(reprlib.Repr): """ subclass of repr.Repr that limits the resulting size of repr() and includes information on exceptions raised during the call. @@ -21,7 +19,7 @@ class SafeRepr(reprlib.Repr): try: # Try the vanilla repr and make sure that the result is a string s = call(x, *args) - except sysex: + except py.builtin._sysex: raise except: cls, e, tb = sys.exc_info() diff --git a/py/_io/terminalwriter.py b/py/_io/terminalwriter.py index c140dbb64..982d5a661 100644 --- a/py/_io/terminalwriter.py +++ b/py/_io/terminalwriter.py @@ -26,7 +26,7 @@ def _getdimensions(): def get_terminal_width(): try: height, width = _getdimensions() - except (SystemExit, KeyboardInterrupt): + except py.builtin._sysex: raise except: # FALLBACK diff --git a/py/_test/collect.py b/py/_test/collect.py index 4e59eb59d..a18aaabdd 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -102,7 +102,7 @@ class Node(object): return getattr(self, attrname) try: res = function() - except (KeyboardInterrupt, SystemExit): + except py.builtin._sysex: raise except: failure = py.std.sys.exc_info() From 79734420df5da760a383ec80a062018cc331f3f6 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 16:18:06 +0200 Subject: [PATCH 12/38] some small doc fixes --HG-- branch : trunk --- CHANGELOG | 2 +- setup.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bf20775f8..00ac14869 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ Changes between 1.3.3 and 1.3.4 - fix issue111: improve install documentation for windows - fix issue116: --doctestmodules works in the presence of __init__.py files as well - fix issue118: new --tb=native option for presenting cpython-standard exceptions -- fix issue115: introduce py.builtin._sysex for system level exceptions we should thread different +- fix issue115: introduce py.builtin._sysex for system level exceptions to let through Changes between 1.3.2 and 1.3.3 ================================================== diff --git a/setup.py b/setup.py index 74d41fb3a..0cde55ed6 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,12 @@ py.test and pylib: rapid testing and development utils - `py.code`_: dynamic code compile and traceback printing support Platforms: Linux, Win32, OSX + Interpreters: Python versions 2.4 through to 3.2, Jython 2.5.1 and PyPy -For questions please check out http://pylib.org/contact.html + +Bugs and issues: http://bitbucket.org/hpk42/py-trunk/issues/ + +Mailing lists and more contact points: http://pylib.org/contact.html .. _`py.test`: http://pytest.org .. _`py.path`: http://pylib.org/path.html From 9ca7ed647bd9d6622813a3e933006692bb38e1ab Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 16:36:34 +0200 Subject: [PATCH 13/38] finalize release announce and changelog --HG-- branch : trunk --- CHANGELOG | 7 ++++--- doc/announce/release-1.3.4.txt | 13 ++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 00ac14869..49a630fc9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,9 +2,10 @@ Changes between 1.3.3 and 1.3.4 ================================================== - fix issue111: improve install documentation for windows -- fix issue116: --doctestmodules works in the presence of __init__.py files as well -- fix issue118: new --tb=native option for presenting cpython-standard exceptions -- fix issue115: introduce py.builtin._sysex for system level exceptions to let through +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- fix issue115: unify internal exception passthrough/catching/GeneratorExit +- fix issue118: new --tb=native for presenting cpython-standard exceptions Changes between 1.3.2 and 1.3.3 ================================================== diff --git a/doc/announce/release-1.3.4.txt b/doc/announce/release-1.3.4.txt index 867a412ff..8fda9d36e 100644 --- a/doc/announce/release-1.3.4.txt +++ b/doc/announce/release-1.3.4.txt @@ -1,8 +1,10 @@ py.test/pylib 1.3.4: fixes and new native traceback option =========================================================================== -pylib/py.test 1.3.4 is a minor bugfix release featuring small fixes. -See changelog_ for full history. +pylib/py.test 1.3.4 is a minor bugfix release featuring small fixes. See +below and the changelog_ for full history. + +Particular thanks to the issue reporters and Ronny Pfannschmidt for general help. have fun, holger krekel @@ -13,6 +15,7 @@ Changes between 1.3.3 and 1.3.4 ================================================== - fix issue111: improve install documentation for windows -- fix issue116: --doctestmodules works in the presence of __init__.py files as well -- fix issue118: new --tb=native option for presenting cpython-standard exceptions - +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- fix issue115: unify internal exception passthrough/catching/GeneratorExit +- fix issue118: new --tb=native for presenting cpython-standard exceptions From 489faf26f29c730019768f1512b4200eb32ddbe0 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 16:54:41 +0200 Subject: [PATCH 14/38] Added tag 1.3.4 for changeset 79ef63777051 --HG-- branch : trunk --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 6cac1da67..ccf5d89af 100644 --- a/.hgtags +++ b/.hgtags @@ -28,3 +28,4 @@ d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1 8b8e7c25a13cf863f01b2dd955978285ae9daf6a 1.3.1 3bff44b188a7ec1af328d977b9d39b6757bb38df 1.3.2 c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 1.3.3 +79ef6377705184c55633d456832eea318fedcf61 1.3.4 From bb6e9848b3a8770b199a085d3341d498bd7b50d1 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 17:35:01 +0200 Subject: [PATCH 15/38] recreated plugin docs --HG-- branch : trunk --- doc/announce/release-1.3.4.txt | 13 +- doc/test/plugin/capturelog.txt | 4 +- doc/test/plugin/cov.txt | 297 +++++++++++++++------------------ doc/test/plugin/coverage.txt | 26 ++- doc/test/plugin/figleaf.txt | 19 ++- doc/test/plugin/helpconfig.txt | 2 +- doc/test/plugin/hookspec.txt | 106 ++++++------ doc/test/plugin/links.txt | 36 ++-- doc/test/plugin/xdist.txt | 6 +- 9 files changed, 261 insertions(+), 248 deletions(-) diff --git a/doc/announce/release-1.3.4.txt b/doc/announce/release-1.3.4.txt index 8fda9d36e..c156c8bdb 100644 --- a/doc/announce/release-1.3.4.txt +++ b/doc/announce/release-1.3.4.txt @@ -1,16 +1,17 @@ py.test/pylib 1.3.4: fixes and new native traceback option =========================================================================== -pylib/py.test 1.3.4 is a minor bugfix release featuring small fixes. See -below and the changelog_ for full history. +pylib/py.test 1.3.4 is a minor maintenance release mostly containing bug fixes +and a new "--tb=native" traceback option to show "normal" Python standard +tracebacks instead of the py.test enhanced tracebacks. See below for more +change info and http://pytest.org for more general information on features +and configuration of the testing tool. -Particular thanks to the issue reporters and Ronny Pfannschmidt for general help. +Thanks to the issue reporters and generally to Ronny Pfannschmidt for help. -have fun, +cheers, holger krekel -.. _changelog: ../changelog.html - Changes between 1.3.3 and 1.3.4 ================================================== diff --git a/doc/test/plugin/capturelog.txt b/doc/test/plugin/capturelog.txt index 0725c39c2..8ca148e68 100644 --- a/doc/test/plugin/capturelog.txt +++ b/doc/test/plugin/capturelog.txt @@ -9,10 +9,10 @@ capture output of logging module. Installation ------------ -You can install the `pytest-capturelog pypi`_ package +You can install the `pytest-capturelog pypi`_ package with pip:: - pip install pytest-capturelog + pip install pytest-capturelog or with easy install:: diff --git a/doc/test/plugin/cov.txt b/doc/test/plugin/cov.txt index 4b91fdb55..3a2548b60 100644 --- a/doc/test/plugin/cov.txt +++ b/doc/test/plugin/cov.txt @@ -6,27 +6,39 @@ produce code coverage reports using the 'coverage' package, including support fo .. contents:: :local: -This plugin produces coverage reports using the coverage package. It -supports centralised testing and distributed testing in both load and -each modes. +This plugin produces coverage reports. It supports centralised testing and distributed testing in +both load and each modes. It also supports coverage of subprocesses. -All features offered by the coverage package should be available, -either through this plugin or through coverage's own config file. +All features offered by the coverage package should be available, either through pytest-cov or +through coverage's config file. Installation ------------ -The `pytest-cov pypi`_ package may be installed / uninstalled with pip:: +The `pytest-cov`_ package may be installed with pip or easy_install:: pip install pytest-cov - pip uninstall pytest-cov - -Alternatively easy_install can be used:: - easy_install pytest-cov -.. _`pytest-cov pypi`: http://pypi.python.org/pypi/pytest-cov/ +.. _`pytest-cov`: http://pypi.python.org/pypi/pytest-cov/ + + +Uninstallation +-------------- + +Uninstalling packages is supported by pip:: + + pip uninstall pytest-cov + +However easy_install does not provide an uninstall facility. + +.. IMPORTANT:: + + Ensure that you manually delete the init_cov_core.pth file in your site-packages directory. + + This file starts coverage collection of subprocesses if appropriate during site initialisation + at python startup. Usage @@ -35,6 +47,9 @@ Usage Centralised Testing ~~~~~~~~~~~~~~~~~~~ +Centralised testing will report on the combined coverage of the main process and all of it's +subprocesses. + Running centralised testing:: py.test --cov myproj tests/ @@ -42,150 +57,149 @@ Running centralised testing:: Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed Testing -~~~~~~~~~~~~~~~~~~~ +Distributed Testing: Load +~~~~~~~~~~~~~~~~~~~~~~~~~ -Distributed testing with dist mode set to load:: +Distributed testing with dist mode set to load will report on the combined coverage of all slaves. +The slaves may be spread out over any number of hosts and each slave may be located anywhere on the +file system. Each slave will have it's subprocesses measured. + +Running distributed testing with dist mode set to load:: py.test --cov myproj -n 2 tests/ -The results from the slaves will be combined like so:: +Shows a terminal report:: -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed testing in each mode:: +Again but spread over different hosts and different directories:: - py.test --cov myproj --dist=each - --tx=popen//python=/usr/local/python265/bin/python - --tx=popen//python=/usr/local/python27b1/bin/python + py.test --cov myproj --dist load + --tx ssh=memedough@host1//chdir=testenv1 + --tx ssh=memedough@host2//chdir=/tmp/testenv2//python=/tmp/env1/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples tests/ -Will produce a report for each slave:: +Shows a terminal report:: - -------------------- coverage: platform linux2, python 2.6.5-final-0 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% - --------------------- coverage: platform linux2, python 2.7.0-beta-1 --------------------- - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% -Distributed testing in each mode can also produce a single combined -report. This is useful to get coverage information spanning things -such as all python versions:: +Distributed Testing: Each +~~~~~~~~~~~~~~~~~~~~~~~~~ - py.test --cov myproj --cov-combine-each --dist=each - --tx=popen//python=/usr/local/python265/bin/python - --tx=popen//python=/usr/local/python27b1/bin/python +Distributed testing with dist mode set to each will report on the combined coverage of all slaves. +Since each slave is running all tests this allows generating a combined coverage report for multiple +environments. + +Running distributed testing with dist mode set to each:: + + py.test --cov myproj --dist each + --tx popen//chdir=/tmp/testenv3//python=/usr/local/python27/bin/python + --tx ssh=memedough@host2//chdir=/tmp/testenv4//python=/tmp/env2/bin/python + --rsyncdir myproj --rsyncdir tests --rsync examples tests/ -Which looks like:: +Shows a terminal report:: ---------------------------------------- coverage ---------------------------------------- platform linux2, python 2.6.5-final-0 - platform linux2, python 2.7.0-beta-1 - Name Stmts Exec Cover Missing - -------------------------------------------------- - myproj/__init__ 2 2 100% - myproj/myproj 257 244 94% 24-26, 99, 149, 233-236, 297-298, 369-370 - myproj/feature4286 94 87 92% 183-188, 197 - -------------------------------------------------- - TOTAL 353 333 94% + platform linux2, python 2.7.0-final-0 + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% Reporting --------- -By default a terminal report is output. This report can be disabled -if desired, such as when results are going to a continuous integration -system and the terminal output won't be seen. +It is possible to generate any combination of the reports for a single test run. -In addition and without rerunning tests it is possible to generate -annotated source code, a html report and an xml report. +The available reports are terminal (with or without missing line numbers shown), HTML, XML and +annotated source code. -The directories for annotated source code and html reports can be -specified as can the file name for the xml report. +The terminal report without line numbers (default):: -Since testing often takes a non trivial amount of time at the end of -testing any / all of the reports may be generated. + py.test --cov-report term --cov myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover + ---------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% + myproj/feature4286 94 7 92% + ---------------------------------------- + TOTAL 353 20 94% + + +The terminal report with line numbers:: + + py.test --cov-report term-missing --cov myproj tests/ + + -------------------- coverage: platform linux2, python 2.6.4-final-0 --------------------- + Name Stmts Miss Cover Missing + -------------------------------------------------- + myproj/__init__ 2 0 100% + myproj/myproj 257 13 94% 24-26, 99, 149, 233-236, 297-298, 369-370 + myproj/feature4286 94 7 92% 183-188, 197 + -------------------------------------------------- + TOTAL 353 20 94% + + +The remaining three reports output to files without showing anything on the terminal (useful for +when the output is going to a continuous integration server):: + + py.test --cov-report html --cov-report xml --cov-report annotate --cov myproj tests/ Coverage Data File ------------------ -During testing there may be many data files with coverage data. These -will have unique suffixes and will be combined at the end of testing. +The data file is erased at the beginning of testing to ensure clean data for each test run. -Upon completion, for --dist=load (and also for --dist=each when the ---cov-combine-each option is used) there will only be one data file. - -For --dist=each there may be many data files where each one will have -the platform / python version info appended to the name. - -These data files are left at the end of testing so that it is possible -to use normal coverage tools to examine them. - -At the beginning of testing any data files that are about to be used -will first be erased so ensure the data is clean for each test run. - -It is possible to set the name of the data file. If needed the -platform / python version will be appended automatically to this name. - - -Coverage Config File --------------------- - -Coverage by default will read its own config file. An alternative -file name may be specified or reading config can be disabled entirely. - -Care has been taken to ensure that the coverage env vars and config -file options work the same under this plugin as they do under coverage -itself. - -Since options may be specified in different ways the order of -precedence between pytest-cov and coverage from highest to lowest is: - -1. pytest command line -2. pytest env var -3. pytest conftest -4. coverage env var -5. coverage config file -6. coverage default +The data file is left at the end of testing so that it is possible to use normal coverage tools to +examine it. Limitations ----------- -For distributed testing the slaves must have the pytest-cov package -installed. This is needed since the plugin must be registered through -setuptools / distribute for pytest to start the plugin on the slave. +For distributed testing the slaves must have the pytest-cov package installed. This is needed since +the plugin must be registered through setuptools / distribute for pytest to start the plugin on the +slave. + +For subprocess measurement environment variables must make it from the main process to the +subprocess. The python used by the subprocess must have pytest-cov installed. The subprocess must +do normal site initialisation so that the environment variables can be detected and coverage +started. Acknowledgements @@ -193,14 +207,11 @@ Acknowledgements Holger Krekel for pytest with its distributed testing support. -Ned Batchelder for coverage and its ability to combine the coverage -results of parallel runs. +Ned Batchelder for coverage and its ability to combine the coverage results of parallel runs. -Whilst this plugin has been built fresh from the ground up to support -distributed testing it has been influenced by the work done on -pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and -nose-cover (Jason Pellerin) which are other coverage plugins for -pytest and nose respectively. +Whilst this plugin has been built fresh from the ground up to support distributed testing it has +been influenced by the work done on pytest-coverage (Ross Lawley, James Mills, Holger Krekel) and +nose-cover (Jason Pellerin) which are other coverage plugins for pytest and nose respectively. No doubt others have contributed to these tools as well. @@ -208,43 +219,11 @@ command line options -------------------- -``--cov-on`` - enable coverage, only needed if not specifying any --cov options -``--cov=package`` - collect coverage for the specified package (multi-allowed) -``--cov-no-terminal`` - disable printing a report on the terminal -``--cov-annotate`` - generate an annotated source code report -``--cov-html`` - generate a html report -``--cov-xml`` - generate an xml report -``--cov-annotate-dir=dir`` - directory for the annotate report, default: %default -``--cov-html-dir=dir`` - directory for the html report, default: coverage_html -``--cov-xml-file=path`` - file for the xml report, default: coverage.xml -``--cov-data-file=path`` - file containing coverage data, default: .coverage -``--cov-combine-each`` - for dist=each mode produce a single combined report -``--cov-branch`` - enable branch coverage -``--cov-pylib`` - enable python library coverage -``--cov-timid`` - enable slower and simpler tracing -``--cov-no-missing-lines`` - disable showing missing lines, only relevant to the terminal report -``--cov-no-missing-files`` - disable showing message about missing source files -``--cov-omit=prefix1,prefix2,...`` - ignore files with these prefixes -``--cov-no-config`` - disable coverage reading its config file -``--cov-config-file=path`` - config file for coverage, default: %default +``--cov=path`` + measure coverage for filesystem path (multi-allowed) +``--cov-report=type`` + type of report to generate: term, term-missing, annotate, html, xml (multi-allowed) +``--cov-config=path`` + config file for coverage, default: .coveragerc .. include:: links.txt diff --git a/doc/test/plugin/coverage.txt b/doc/test/plugin/coverage.txt index e22bab43b..965b4a4ee 100644 --- a/doc/test/plugin/coverage.txt +++ b/doc/test/plugin/coverage.txt @@ -6,10 +6,27 @@ Write and report coverage data with the 'coverage' package. .. contents:: :local: -Original code by Ross Lawley. +Note: Original code by Ross Lawley. -Requires Ned Batchelder's excellent coverage: -http://nedbatchelder.com/code/coverage/ +Install +-------------- + +Use pip to (un)install:: + + pip install pytest-coverage + pip uninstall pytest-coverage + +or alternatively use easy_install to install:: + + easy_install pytest-coverage + + +Usage +------------- + +To get full test coverage reports for a particular package type:: + + py.test --cover-report=report command line options -------------------- @@ -21,8 +38,11 @@ command line options html: Directory for html output. report: Output a text report. annotate: Annotate your source code for which lines were executed and which were not. + xml: Output an xml report compatible with the cobertura plugin for hudson. ``--cover-directory=DIRECTORY`` Directory for the reports (html / annotate results) defaults to ./coverage +``--cover-xml-file=XML_FILE`` + File for the xml report defaults to ./coverage.xml ``--cover-show-missing`` Show missing files ``--cover-ignore-errors=IGNORE_ERRORS`` diff --git a/doc/test/plugin/figleaf.txt b/doc/test/plugin/figleaf.txt index c2fe6f0b3..86e0da65b 100644 --- a/doc/test/plugin/figleaf.txt +++ b/doc/test/plugin/figleaf.txt @@ -6,16 +6,29 @@ report test coverage using the 'figleaf' package. .. contents:: :local: +Install +--------------- + +To install the plugin issue:: + + easy_install pytest-figleaf # or + pip install pytest-figleaf + +and if you are using pip you can also uninstall:: + + pip uninstall pytest-figleaf + + Usage --------------- -after pip or easy_install mediated installation of ``pytest-figleaf`` you can type:: +After installation you can simply type:: py.test --figleaf [...] to enable figleaf coverage in your test run. A default ".figleaf" data file -and "html" directory will be created. You can use ``--fig-data`` -and ``fig-html`` to modify the paths. +and "html" directory will be created. You can use command line options +to control where data and html files are created. command line options -------------------- diff --git a/doc/test/plugin/helpconfig.txt b/doc/test/plugin/helpconfig.txt index 5c307a7bb..966bbc988 100644 --- a/doc/test/plugin/helpconfig.txt +++ b/doc/test/plugin/helpconfig.txt @@ -19,7 +19,7 @@ command line options ``--traceconfig`` trace considerations of conftest.py files. ``--nomagic`` - don't reinterpret asserts, no traceback cutting. + don't reinterpret asserts, no traceback cutting. ``--debug`` generate and show internal debugging information. ``--help-config`` diff --git a/doc/test/plugin/hookspec.txt b/doc/test/plugin/hookspec.txt index d56501d54..3f52d9bb4 100644 --- a/doc/test/plugin/hookspec.txt +++ b/doc/test/plugin/hookspec.txt @@ -7,67 +7,67 @@ hook specification sourcecode """ hook specifications for py.test plugins """ - + # ------------------------------------------------------------------------- # Command line and configuration # ------------------------------------------------------------------------- - + def pytest_namespace(): "return dict of name->object which will get stored at py.test. namespace" - + def pytest_addoption(parser): "add optparse-style options via parser.addoption." - + def pytest_addhooks(pluginmanager): "add hooks via pluginmanager.registerhooks(module)" - + def pytest_configure(config): """ called after command line options have been parsed. and all plugins and initial conftest files been loaded. """ - + def pytest_unconfigure(config): """ called before test process is exited. """ - + # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- - + def pytest_ignore_collect(path, config): """ return true value to prevent considering this path for collection. This hook is consulted for all files and directories prior to considering collection hooks. """ pytest_ignore_collect.firstresult = True - + def pytest_collect_directory(path, parent): """ return Collection node or None for the given path. """ pytest_collect_directory.firstresult = True - + def pytest_collect_file(path, parent): """ return Collection node or None for the given path. """ - + def pytest_collectstart(collector): """ collector starts collecting. """ - + def pytest_collectreport(report): """ collector finished collecting. """ - + def pytest_deselected(items): """ called for test items deselected by keyword. """ - + def pytest_make_collect_report(collector): """ perform a collection and return a collection. """ pytest_make_collect_report.firstresult = True - + # XXX rename to item_collected()? meaning in distribution context? def pytest_itemstart(item, node=None): """ test item gets collected. """ - + # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- - + def pytest_pycollect_makemodule(path, parent): """ return a Module collector or None for the given path. This hook will be called for each matching test module path. @@ -75,105 +75,105 @@ hook specification sourcecode create test modules for files that do not match as a test module. """ pytest_pycollect_makemodule.firstresult = True - + def pytest_pycollect_makeitem(collector, name, obj): """ return custom item/collector for a python object in a module, or None. """ pytest_pycollect_makeitem.firstresult = True - + def pytest_pyfunc_call(pyfuncitem): """ call underlying test function. """ pytest_pyfunc_call.firstresult = True - + def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" - + # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- - + def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True - + def pytest_runtest_setup(item): """ called before pytest_runtest_call(). """ - + def pytest_runtest_call(item): """ execute test item. """ - + def pytest_runtest_teardown(item): """ called after pytest_runtest_call(). """ - + def pytest_runtest_makereport(item, call): """ make a test report for the given item and call outcome. """ pytest_runtest_makereport.firstresult = True - + def pytest_runtest_logreport(report): """ process item test report. """ - + # special handling for final teardown - somewhat internal for now def pytest__teardown_final(session): """ called before test session finishes. """ pytest__teardown_final.firstresult = True - + def pytest__teardown_final_logerror(report): """ called if runtest_teardown_final failed. """ - + # ------------------------------------------------------------------------- # test session related hooks # ------------------------------------------------------------------------- - + def pytest_sessionstart(session): """ before session.main() is called. """ - + def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ - + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- - + def pytest_report_header(config): """ return a string to be displayed as header info for terminal reporting.""" - + def pytest_report_teststatus(report): """ return result-category, shortletter and verbose word for reporting.""" pytest_report_teststatus.firstresult = True - + def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ - + def pytest_report_iteminfo(item): """ return (fspath, lineno, name) for the item. the information is used for result display and to sort tests """ pytest_report_iteminfo.firstresult = True - + # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- - + def pytest_doctest_prepare_content(content): """ return processed content for a given doctest""" pytest_doctest_prepare_content.firstresult = True - - + + # ------------------------------------------------------------------------- # error handling and internal debugging hooks # ------------------------------------------------------------------------- - + def pytest_plugin_registered(plugin, manager): """ a new py lib plugin got registered. """ - + def pytest_plugin_unregistered(plugin): """ a py lib plugin got unregistered. """ - + def pytest_internalerror(excrepr): """ called for internal errors. """ - + def pytest_keyboard_interrupt(excinfo): """ called for keyboard interrupt. """ - + def pytest_trace(category, msg): """ called for debug info. """ @@ -182,25 +182,25 @@ hook specification sourcecode .. sourcecode:: python - + def pytest_gwmanage_newgateway(gateway, platinfo): """ called on new raw gateway creation. """ - + def pytest_gwmanage_rsyncstart(source, gateways): """ called before rsyncing a directory to remote gateways takes place. """ - + def pytest_gwmanage_rsyncfinish(source, gateways): """ called after rsyncing a directory to remote gateways takes place. """ - + def pytest_configure_node(node): """ configure node information before it gets instantiated. """ - + def pytest_testnodeready(node): """ Test Node is ready to operate. """ - + def pytest_testnodedown(node, error): """ Test Node is down. """ - + def pytest_rescheduleitems(items): """ reschedule Items from a node that went down. """ diff --git a/doc/test/plugin/links.txt b/doc/test/plugin/links.txt index 568c74f29..b7d427856 100644 --- a/doc/test/plugin/links.txt +++ b/doc/test/plugin/links.txt @@ -1,47 +1,47 @@ .. _`helpconfig`: helpconfig.html -.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_recwarn.py +.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_recwarn.py .. _`unittest`: unittest.html -.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_monkeypatch.py -.. _`pytest_genscript.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_genscript.py +.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_monkeypatch.py +.. _`pytest_genscript.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_genscript.py .. _`pastebin`: pastebin.html .. _`skipping`: skipping.html .. _`genscript`: genscript.html .. _`plugins`: index.html .. _`mark`: mark.html .. _`tmpdir`: tmpdir.html -.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_doctest.py +.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_doctest.py .. _`capture`: capture.html -.. _`pytest_nose.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_nose.py -.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_restdoc.py +.. _`pytest_nose.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_nose.py +.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_restdoc.py .. _`restdoc`: restdoc.html .. _`xdist`: xdist.html -.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_pastebin.py -.. _`pytest_tmpdir.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_tmpdir.py +.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_pastebin.py +.. _`pytest_tmpdir.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_tmpdir.py .. _`terminal`: terminal.html -.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_hooklog.py +.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_hooklog.py .. _`capturelog`: capturelog.html .. _`junitxml`: junitxml.html -.. _`pytest_skipping.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_skipping.py +.. _`pytest_skipping.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_skipping.py .. _`checkout the py.test development version`: ../../install.html#checkout -.. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_helpconfig.py +.. _`pytest_helpconfig.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_helpconfig.py .. _`oejskit`: oejskit.html .. _`doctest`: doctest.html -.. _`pytest_mark.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_mark.py +.. _`pytest_mark.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_mark.py .. _`get in contact`: ../../contact.html -.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_capture.py +.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_capture.py .. _`figleaf`: figleaf.html .. _`customize`: ../customize.html .. _`hooklog`: hooklog.html -.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_terminal.py +.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_terminal.py .. _`recwarn`: recwarn.html -.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_pdb.py +.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_pdb.py .. _`monkeypatch`: monkeypatch.html .. _`coverage`: coverage.html .. _`resultlog`: resultlog.html .. _`cov`: cov.html -.. _`pytest_junitxml.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_junitxml.py +.. _`pytest_junitxml.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_junitxml.py .. _`django`: django.html -.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_unittest.py +.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_unittest.py .. _`nose`: nose.html -.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.2/py/_plugin/pytest_resultlog.py +.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/1.3.4/py/_plugin/pytest_resultlog.py .. _`pdb`: pdb.html diff --git a/doc/test/plugin/xdist.txt b/doc/test/plugin/xdist.txt index 5d05ccc91..4dbcc78c7 100644 --- a/doc/test/plugin/xdist.txt +++ b/doc/test/plugin/xdist.txt @@ -156,11 +156,11 @@ command line options box each test run in a separate process (unix) ``--dist=distmode`` set mode for distributing tests to exec environments. - + each: send each test to each available environment. - + load: send each test to available environment. - + (default) no: run tests inprocess, don't distribute. ``--tx=xspec`` add a test execution environment. some examples: --tx popen//python=python2.5 --tx socket=192.168.1.102:8888 --tx ssh=user@codespeak.net//chdir=testcache From 350ebbd9ad5b1f01363abc84fb7dd8c70417dc86 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 14 Sep 2010 17:35:17 +0200 Subject: [PATCH 16/38] Added tag 1.3.4 for changeset 90fffd35373e --HG-- branch : trunk --- .hgtags | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.hgtags b/.hgtags index ccf5d89af..eadb1993b 100644 --- a/.hgtags +++ b/.hgtags @@ -29,3 +29,5 @@ d5eacf390af74553227122b85e20345d47b2f9e6 1.3.1 3bff44b188a7ec1af328d977b9d39b6757bb38df 1.3.2 c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 1.3.3 79ef6377705184c55633d456832eea318fedcf61 1.3.4 +79ef6377705184c55633d456832eea318fedcf61 1.3.4 +90fffd35373e9f125af233f78b19416f0938d841 1.3.4 From e2683f4538d8faf663be5acc3874051c3486adde Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 15 Sep 2010 10:30:50 +0200 Subject: [PATCH 17/38] refactor all collection related logic - drop all pickling support (for now) - perform collection completely ahead of test running (no iterativity) - introduce new collection related hooks - shift all keyword-selection code to pytest_keyword plugin - simplify session object - besides: fix issue88 --HG-- branch : trunk --- CHANGELOG | 7 + py/__init__.py | 4 +- py/_plugin/hookspec.py | 24 +- py/_plugin/pytest__pytest.py | 22 ++ py/_plugin/pytest_default.py | 33 +- py/_plugin/pytest_keyword.py | 66 ++++ py/_plugin/pytest_pytester.py | 51 ++- py/_plugin/pytest_terminal.py | 64 +--- py/_test/cmdline.py | 24 -- py/_test/collect.py | 143 +------- py/_test/config.py | 99 +----- py/_test/funcargs.py | 42 +++ py/_test/pluginmanager.py | 2 +- py/_test/pycollect.py | 5 +- py/_test/session.py | 247 ++++++++++---- setup.py | 2 +- testing/acceptance_test.py | 32 +- testing/plugin/test_pytest_genscript.py | 1 + .../test_pytest_keyword.py} | 0 testing/plugin/test_pytest_pytester.py | 2 + testing/plugin/test_pytest_resultlog.py | 6 +- testing/plugin/test_pytest_skipping.py | 2 +- testing/plugin/test_pytest_terminal.py | 21 +- testing/test_collect.py | 99 +----- testing/test_collection.py | 314 ++++++++++++++++++ testing/test_config.py | 230 ------------- testing/test_conftesthandle.py | 8 +- testing/test_deprecated_api.py | 26 +- testing/test_pycollect.py | 9 +- testing/test_session.py | 15 +- 30 files changed, 819 insertions(+), 781 deletions(-) create mode 100644 py/_plugin/pytest_keyword.py delete mode 100644 py/_test/cmdline.py rename testing/{test_genitems.py => plugin/test_pytest_keyword.py} (100%) create mode 100644 testing/test_collection.py diff --git a/CHANGELOG b/CHANGELOG index 49a630fc9..d9cf05000 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ + +Changes between 1.3.4 and 1.4.0a1 +================================================== + +- major refactoring of internal collection handling +- fix issue88 (finding custom test nodes from command line arg) + Changes between 1.3.3 and 1.3.4 ================================================== diff --git a/py/__init__.py b/py/__init__.py index c6e95d21d..0a831a41c 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = version = "1.3.4" +__version__ = version = "1.4.0a1" import py.apipkg @@ -53,7 +53,7 @@ py.apipkg.initpkg(__name__, dict( '_fillfuncargs' : '._test.funcargs:fillfuncargs', }, 'cmdline': { - 'main' : '._test.cmdline:main', # backward compat + 'main' : '._test.session:main', # backward compat }, }, diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 04e1bd0d2..925d4a5aa 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -20,6 +20,10 @@ def pytest_configure(config): and all plugins and initial conftest files been loaded. """ +def pytest_cmdline_main(config): + """ called for performing the main (cmdline) action. """ +pytest_cmdline_main.firstresult = True + def pytest_unconfigure(config): """ called before test process is exited. """ @@ -27,6 +31,15 @@ def pytest_unconfigure(config): # collection hooks # ------------------------------------------------------------------------- +def pytest_log_startcollection(collection): + """ called before collection.perform_collection() is called. """ + +def pytest_collection_modifyitems(collection): + """ called to allow filtering and selecting of test items (inplace). """ + +def pytest_log_finishcollection(collection): + """ called after collection has finished. """ + def pytest_ignore_collect(path, config): """ return true value to prevent considering this path for collection. This hook is consulted for all files and directories prior to considering @@ -41,9 +54,13 @@ pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): """ return Collection node or None for the given path. """ +# logging hooks for collection def pytest_collectstart(collector): """ collector starts collecting. """ +def pytest_log_itemcollect(item): + """ we just collected a test item. """ + def pytest_collectreport(report): """ collector finished collecting. """ @@ -54,10 +71,6 @@ def pytest_make_collect_report(collector): """ perform a collection and return a collection. """ pytest_make_collect_report.firstresult = True -# XXX rename to item_collected()? meaning in distribution context? -def pytest_itemstart(item, node=None): - """ test item gets collected. """ - # ------------------------------------------------------------------------- # Python test function related hooks # ------------------------------------------------------------------------- @@ -85,6 +98,9 @@ def pytest_generate_tests(metafunc): # generic runtest related hooks # ------------------------------------------------------------------------- +def pytest_itemstart(item, node=None): + """ test item starts running. """ + def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True diff --git a/py/_plugin/pytest__pytest.py b/py/_plugin/pytest__pytest.py index 5d82edf63..f5846a3e5 100644 --- a/py/_plugin/pytest__pytest.py +++ b/py/_plugin/pytest__pytest.py @@ -87,6 +87,28 @@ class HookRecorder: l.append(call) return l + def contains(self, entries): + from py.builtin import print_ + i = 0 + entries = list(entries) + backlocals = py.std.sys._getframe(1).f_locals + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print_("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print_("CHECKERMATCH", repr(check), "->", call) + else: + print_("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print_("NONAMEMATCH", name, "with", call) + else: + raise AssertionError("could not find %r in %r" %( + name, self.calls[i:])) + def popcall(self, name): for i, call in enumerate(self.calls): if call._name == name: diff --git a/py/_plugin/pytest_default.py b/py/_plugin/pytest_default.py index 2ced855a0..41f320754 100644 --- a/py/_plugin/pytest_default.py +++ b/py/_plugin/pytest_default.py @@ -3,6 +3,22 @@ import sys import py +def pytest_cmdline_main(config): + from py._test.session import Session, Collection + exitstatus = 0 + if config.option.showfuncargs: + from py._test.funcargs import showfuncargs + session = showfuncargs(config) + else: + collection = Collection(config) + # instantiate session already because it + # records failures and implements maxfail handling + session = Session(config, collection) + exitstatus = collection.do_collection() + if not exitstatus: + exitstatus = session.main() + return exitstatus + def pytest_pyfunc_call(__multicall__, pyfuncitem): if not __multicall__.execute(): testfunction = pyfuncitem.obj @@ -16,7 +32,7 @@ def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.config._argfspaths: + path in parent.collection._argfspaths: if ext == ".py": return parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) @@ -49,7 +65,7 @@ def pytest_collect_directory(path, parent): # define Directory(dir) already if not parent.recfilter(path): # by default special ".cvs", ... # check if cmdline specified this dir or a subdir directly - for arg in parent.config._argfspaths: + for arg in parent.collection._argfspaths: if path == arg or arg.relto(path): break else: @@ -68,12 +84,6 @@ def pytest_addoption(parser): group._addoption('--maxfail', metavar="num", action="store", type="int", dest="maxfail", default=0, help="exit after first num failures or errors.") - 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. ") group = parser.getgroup("collect", "collection") group.addoption('--collectonly', @@ -91,17 +101,10 @@ def pytest_addoption(parser): help="base temporary directory for this test run.") def pytest_configure(config): - setsession(config) # compat if config.getvalue("exitfirst"): config.option.maxfail = 1 -def setsession(config): - val = config.getvalue - if val("collectonly"): - from py._test.session import Session - config.setsessionclass(Session) - # pycollect related hooks and code, should move to pytest_pycollect.py def pytest_pycollect_makeitem(__multicall__, collector, name, obj): diff --git a/py/_plugin/pytest_keyword.py b/py/_plugin/pytest_keyword.py new file mode 100644 index 000000000..35ee6cd81 --- /dev/null +++ b/py/_plugin/pytest_keyword.py @@ -0,0 +1,66 @@ + +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. ") + +def pytest_collection_modifyitems(collection): + config = collection.config + keywordexpr = config.option.keyword + if not keywordexpr: + return + selectuntil = False + if keywordexpr[-1] == ":": + selectuntil = True + keywordexpr = keywordexpr[:-1] + + remaining = [] + deselected = [] + for colitem in collection.items: + if keywordexpr and skipbykeyword(colitem, keywordexpr): + deselected.append(colitem) + else: + remaining.append(colitem) + if selectuntil: + keywordexpr = None + + if deselected: + config.hook.pytest_deselected(items=deselected) + collection.items[:] = remaining + +def skipbykeyword(colitem, keywordexpr): + """ return True if they given keyword expression means to + skip this collector/item. + """ + if not keywordexpr: + return + chain = colitem.listchain() + for key in filter(None, keywordexpr.split()): + eor = key[:1] == '-' + if eor: + key = key[1:] + if not (eor ^ matchonekeyword(key, chain)): + 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: + break + if num == len(elems) - 1 and ok: + return True + return False diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index d96c4c802..18df368c5 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -74,10 +74,8 @@ class TmpTestdir: def __repr__(self): return "" % (self.tmpdir,) - def Config(self, topdir=None): - if topdir is None: - topdir = self.tmpdir.dirpath() - return pytestConfig(topdir=topdir) + def Config(self): + return pytestConfig() def finalize(self): for p in self._syspathremove: @@ -149,16 +147,23 @@ class TmpTestdir: p.ensure("__init__.py") return p + def getnode(self, config, arg): + from py._test.session import Collection + collection = Collection(config) + return collection.getbyid(collection._normalizearg(arg))[0] + def genitems(self, colitems): - return list(self.session.genitems(colitems)) + collection = colitems[0].collection + result = [] + collection.genitems(colitems, (), result) + return result def inline_genitems(self, *args): #config = self.parseconfig(*args) - config = self.parseconfig(*args) - session = config.initsession() + from py._test.session import Collection + config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - colitems = [config.getnode(arg) for arg in config.args] - items = list(session.genitems(colitems)) + items = Collection(config).perform_collect() return items, rec def runitem(self, source): @@ -187,11 +192,9 @@ class TmpTestdir: def inline_run(self, *args): args = ("-s", ) + args # otherwise FD leakage config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) - session = config.initsession() reprec = self.getreportrecorder(config) - colitems = config.getinitialnodes() - session.main(colitems) + config.pluginmanager.do_configure(config) + config.hook.pytest_cmdline_main(config=config) config.pluginmanager.do_unconfigure(config) return reprec @@ -245,29 +248,17 @@ class TmpTestdir: def getitems(self, source): modcol = self.getmodulecol(source) - return list(modcol.config.initsession().genitems([modcol])) - #assert item is not None, "%r item not found in module:\n%s" %(funcname, source) - #return item - - def getfscol(self, path, configargs=()): - self.config = self.parseconfig(path, *configargs) - self.session = self.config.initsession() - return self.config.getnode(path) + return self.genitems([modcol]) def getmodulecol(self, source, configargs=(), withinit=False): kw = {self.request.function.__name__: py.code.Source(source).strip()} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__ = "#") - self.config = self.parseconfig(path, *configargs) - self.session = self.config.initsession() - #self.config.pluginmanager.do_configure(config=self.config) - # XXX - self.config.pluginmanager.import_plugin("runner") - plugin = self.config.pluginmanager.getplugin("runner") - plugin.pytest_configure(config=self.config) - - return self.config.getnode(path) + self.config = config = self.parseconfigure(path, *configargs) + node = self.getnode(config, path) + #config.pluginmanager.do_unconfigure(config) + return node def popen(self, cmdargs, stdout, stderr, **kw): if not hasattr(py.std, 'subprocess'): diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index 6487c0662..7fac140bb 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -35,7 +35,6 @@ def pytest_configure(config): if config.option.collectonly: reporter = CollectonlyReporter(config) elif config.option.showfuncargs: - config.setsessionclass(ShowFuncargSession) reporter = None else: reporter = TerminalReporter(config) @@ -170,7 +169,7 @@ class TerminalReporter: self.write_line("[%s] %s" %(category, msg)) def pytest_deselected(self, items): - self.stats.setdefault('deselected', []).append(items) + self.stats.setdefault('deselected', []).extend(items) def pytest_itemstart(self, item, node=None): if self.config.option.verbose: @@ -383,21 +382,27 @@ class CollectonlyReporter: self.outindent(collector) self.indent += self.INDENT - def pytest_itemstart(self, item, node=None): + def pytest_log_itemcollect(self, item): self.outindent(item) def pytest_collectreport(self, report): if not report.passed: - self.outindent("!!! %s !!!" % report.longrepr.reprcrash.message) + if hasattr(report.longrepr, 'reprcrash'): + msg = report.longrepr.reprcrash.message + else: + # XXX unify (we have CollectErrorRepr here) + msg = str(report.longrepr.longrepr) + self.outindent("!!! %s !!!" % msg) + #self.outindent("!!! error !!!") self._failed.append(report) self.indent = self.indent[:-len(self.INDENT)] - def pytest_sessionfinish(self, session, exitstatus): + def pytest_log_finishcollection(self): if self._failed: self._tw.sep("!", "collection failures") for rep in self._failed: rep.toterminal(self._tw) - + return self._failed and 1 or 0 def repr_pythonversion(v=None): if v is None: @@ -415,50 +420,3 @@ def flatten(l): else: yield x -from py._test.session import Session -class ShowFuncargSession(Session): - def main(self, colitems): - self.fspath = py.path.local() - self.sessionstarts() - try: - self.showargs(colitems[0]) - finally: - self.sessionfinishes(exitstatus=1) - - def showargs(self, colitem): - tw = py.io.TerminalWriter() - from py._test.funcargs import getplugins - from py._test.funcargs import FuncargRequest - plugins = getplugins(colitem, withpy=True) - verbose = self.config.getvalue("verbose") - for plugin in plugins: - available = [] - for name, factory in vars(plugin).items(): - if name.startswith(FuncargRequest._argprefix): - name = name[len(FuncargRequest._argprefix):] - if name not in available: - available.append([name, factory]) - if available: - pluginname = plugin.__name__ - for name, factory in available: - loc = self.getlocation(factory) - if verbose: - funcargspec = "%s -- %s" %(name, loc,) - else: - funcargspec = name - tw.line(funcargspec, green=True) - doc = factory.__doc__ or "" - if doc: - for line in doc.split("\n"): - tw.line(" " + line.strip()) - else: - tw.line(" %s: no docstring available" %(loc,), - red=True) - - def getlocation(self, function): - import inspect - fn = py.path.local(inspect.getfile(function)) - lineno = py.builtin._getcode(function).co_firstlineno - if fn.relto(self.fspath): - fn = fn.relto(self.fspath) - return "%s:%d" %(fn, lineno+1) diff --git a/py/_test/cmdline.py b/py/_test/cmdline.py deleted file mode 100644 index 10f13f795..000000000 --- a/py/_test/cmdline.py +++ /dev/null @@ -1,24 +0,0 @@ -import py -import sys - -# -# main entry point -# - -def main(args=None): - if args is None: - args = sys.argv[1:] - config = py.test.config - try: - config.parse(args) - config.pluginmanager.do_configure(config) - session = config.initsession() - colitems = config.getinitialnodes() - exitstatus = session.main(colitems) - config.pluginmanager.do_unconfigure(config) - except config.Error: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - py.test.config = py.test.config.__class__() - return exitstatus diff --git a/py/_test/collect.py b/py/_test/collect.py index a18aaabdd..e2bd650db 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -25,47 +25,15 @@ class Node(object): """ base class for all Nodes in the collection tree. Collector subclasses have children, Items are terminal nodes. """ - def __init__(self, name, parent=None, config=None): + def __init__(self, name, parent=None, config=None, collection=None): self.name = name self.parent = parent self.config = config or parent.config + self.collection = collection or getattr(parent, 'collection', None) self.fspath = getattr(parent, 'fspath', None) self.ihook = HookProxy(self) self.keywords = self.readkeywords() - def _reraiseunpicklingproblem(self): - if hasattr(self, '_unpickle_exc'): - py.builtin._reraise(*self._unpickle_exc) - - # - # note to myself: Pickling is uh. - # - def __getstate__(self): - return (self.name, self.parent) - def __setstate__(self, nameparent): - name, parent = nameparent - try: - colitems = parent._memocollect() - for colitem in colitems: - if colitem.name == name: - # we are a copy that will not be returned - # by our parent - self.__dict__ = colitem.__dict__ - break - else: - raise ValueError("item %r not found in parent collection %r" %( - name, [x.name for x in colitems])) - except KeyboardInterrupt: - raise - except Exception: - # our parent can't collect us but we want unpickling to - # otherwise continue - self._reraiseunpicklingproblem() will - # reraise the problem - self._unpickle_exc = py.std.sys.exc_info() - self.name = name - self.parent = parent - self.config = parent.config - def __repr__(self): if getattr(self.config.option, 'debug', False): return "<%s %r %0x>" %(self.__class__.__name__, @@ -79,7 +47,8 @@ class Node(object): def __eq__(self, other): if not isinstance(other, Node): return False - return self.name == other.name and self.parent == other.parent + return self.__class__ == other.__class__ and \ + self.name == other.name and self.parent == other.parent def __ne__(self, other): return not self == other @@ -117,7 +86,7 @@ class Node(object): l = [self] while 1: x = l[0] - if x.parent is not None and x.parent.parent is not None: + if x.parent is not None: # and x.parent.parent is not None: l.insert(0, x.parent) else: return l @@ -137,39 +106,6 @@ class Node(object): def _keywords(self): return [self.name] - def _skipbykeyword(self, keywordexpr): - """ return True if they given keyword expression means to - skip this collector/item. - """ - if not keywordexpr: - return - chain = self.listchain() - for key in filter(None, keywordexpr.split()): - eor = key[:1] == '-' - if eor: - key = key[1:] - if not (eor ^ self._matchonekeyword(key, chain)): - return True - - def _matchonekeyword(self, 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: - break - if num == len(elems) - 1 and ok: - return True - return False - def _prunetraceback(self, traceback): return traceback @@ -270,19 +206,12 @@ class Collector(Node): return traceback class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None): + def __init__(self, fspath, parent=None, config=None, collection=None): fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, parent, config=config) + super(FSCollector, self).__init__(fspath.basename, + parent, config, collection) self.fspath = fspath - def __getstate__(self): - # RootCollector.getbynames() inserts a directory which we need - # to throw out here for proper re-instantiation - if isinstance(self.parent.parent, RootCollector): - assert self.parent.fspath == self.parent.parent.fspath, self.parent - return (self.name, self.parent.parent) # shortcut - return super(Collector, self).__getstate__() - class File(FSCollector): """ base class for collecting tests from a file. """ @@ -368,59 +297,3 @@ def warnoldtestrun(function=None): "item.run() and item.execute()", stacklevel=2, function=function) - - -class RootCollector(Directory): - def __init__(self, config): - Directory.__init__(self, config.topdir, parent=None, config=config) - self.name = None - - def __repr__(self): - return "" %(self.fspath,) - - def getbynames(self, names): - current = self.consider(self.config.topdir) - while names: - name = names.pop(0) - if name == ".": # special "identity" name - continue - l = [] - for x in current._memocollect(): - if x.name == name: - l.append(x) - elif x.fspath == current.fspath.join(name): - l.append(x) - elif x.name == "()": - names.insert(0, name) - l.append(x) - break - if not l: - raise ValueError("no node named %r below %r" %(name, current)) - current = l[0] - return current - - def totrail(self, node): - chain = node.listchain() - names = [self._getrelpath(chain[0].fspath)] - names += [x.name for x in chain[1:]] - return names - - def fromtrail(self, trail): - return self.config._rootcol.getbynames(trail) - - def _getrelpath(self, fspath): - topdir = self.config.topdir - relpath = fspath.relto(topdir) - if not relpath: - if fspath == topdir: - relpath = "." - else: - raise ValueError("%r not relative to topdir %s" - %(self.fspath, topdir)) - return relpath - - def __getstate__(self): - return self.config - - def __setstate__(self, config): - self.__init__(config) diff --git a/py/_test/config.py b/py/_test/config.py index d72ddb722..47ffd4f09 100644 --- a/py/_test/config.py +++ b/py/_test/config.py @@ -2,7 +2,6 @@ import py, os from py._test.conftesthandle import Conftest from py._test.pluginmanager import PluginManager from py._test import parseopt -from py._test.collect import RootCollector def ensuretemp(string, dir=1): """ (deprecated) return temporary directory path with @@ -31,9 +30,8 @@ class Config(object): basetemp = None _sessionclass = None - def __init__(self, topdir=None, option=None): - self.option = option or CmdOptions() - self.topdir = topdir + def __init__(self): + self.option = CmdOptions() self._parser = parseopt.Parser( usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", processopt=self._processopt, @@ -97,39 +95,7 @@ class Config(object): args = self._parser.parse_setoption(args, self.option) if not args: args.append(py.std.os.getcwd()) - self.topdir = gettopdir(args) - self._rootcol = RootCollector(config=self) - self._setargs(args) - - def _setargs(self, args): - self.args = list(args) - self._argfspaths = [py.path.local(decodearg(x)[0]) for x in args] - - # config objects are usually pickled across system - # barriers but they contain filesystem paths. - # upon getstate/setstate we take care to do everything - # relative to "topdir". - def __getstate__(self): - l = [] - for path in self.args: - path = py.path.local(path) - l.append(path.relto(self.topdir)) - return l, self.option.__dict__ - - def __setstate__(self, repr): - # we have to set py.test.config because loading - # of conftest files may use it (deprecated) - # mainly by py.test.config.addoptions() - global config_per_process - py.test.config = config_per_process = self - args, cmdlineopts = repr - cmdlineopts = CmdOptions(**cmdlineopts) - # next line will registers default plugins - self.__init__(topdir=py.path.local(), option=cmdlineopts) - self._rootcol = RootCollector(config=self) - args = [str(self.topdir.join(x)) for x in args] - self._preparse(args) - self._setargs(args) + self.args = args def ensuretemp(self, string, dir=True): return self.getbasetemp().ensure(string, dir=dir) @@ -154,27 +120,6 @@ class Config(object): return py.path.local.make_numbered_dir(prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None) - def getinitialnodes(self): - return [self.getnode(arg) for arg in self.args] - - def getnode(self, arg): - parts = decodearg(arg) - path = py.path.local(parts.pop(0)) - if not path.check(): - raise self.Error("file not found: %s" %(path,)) - topdir = self.topdir - if path != topdir and not path.relto(topdir): - raise self.Error("path %r is not relative to %r" % - (str(path), str(topdir))) - # assumtion: pytest's fs-collector tree follows the filesystem tree - names = list(filter(None, path.relto(topdir).split(path.sep))) - names += parts - try: - return self._rootcol.getbynames(names) - except ValueError: - e = py.std.sys.exc_info()[1] - raise self.Error("can't collect: %s\n%s" % (arg, e.args[0])) - def _getcollectclass(self, name, path): try: cls = self._conftest.rget(name, path) @@ -239,48 +184,10 @@ class Config(object): except AttributeError: return self._conftest.rget(name, path) - def setsessionclass(self, cls): - if self._sessionclass is not None: - raise ValueError("sessionclass already set to: %r" %( - self._sessionclass)) - self._sessionclass = cls - - def initsession(self): - """ return an initialized session object. """ - cls = self._sessionclass - if cls is None: - from py._test.session import Session - cls = Session - session = cls(self) - self.trace("instantiated session %r" % session) - return session - # # helpers # -def gettopdir(args): - """ return the top directory for the given paths. - if the common base dir resides in a python package - parent directory of the root package is returned. - """ - fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] - p = fsargs and fsargs[0] or None - for x in fsargs[1:]: - p = p.common(x) - assert p, "cannot determine common basedir of %s" %(fsargs,) - pkgdir = p.pypkgpath() - if pkgdir is None: - if p.check(file=1): - p = p.dirpath() - return p - else: - return pkgdir.dirpath() - -def decodearg(arg): - arg = str(arg) - return arg.split("::") - def onpytestaccess(): # it's enough to have our containing module loaded as # it initializes a per-process config instance diff --git a/py/_test/funcargs.py b/py/_test/funcargs.py index 5d69b1939..2b4f9a0ca 100644 --- a/py/_test/funcargs.py +++ b/py/_test/funcargs.py @@ -184,3 +184,45 @@ class FuncargRequest: msg += "\n available funcargs: %s" %(", ".join(available),) msg += "\n use 'py.test --funcargs [testpath]' for help on them." raise self.LookupError(msg) + +def showfuncargs(config): + from py._test.session import Collection + collection = Collection(config) + colitem = collection.getinitialnodes()[0] + curdir = py.path.local() + tw = py.io.TerminalWriter() + #from py._test.funcargs import getplugins + #from py._test.funcargs import FuncargRequest + plugins = getplugins(colitem, withpy=True) + verbose = config.getvalue("verbose") + for plugin in plugins: + available = [] + for name, factory in vars(plugin).items(): + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] + if name not in available: + available.append([name, factory]) + if available: + pluginname = plugin.__name__ + for name, factory in available: + loc = getlocation(factory, curdir) + if verbose: + funcargspec = "%s -- %s" %(name, loc,) + else: + funcargspec = name + tw.line(funcargspec, green=True) + doc = factory.__doc__ or "" + if doc: + for line in doc.split("\n"): + tw.line(" " + line.strip()) + else: + tw.line(" %s: no docstring available" %(loc,), + red=True) + +def getlocation(function, curdir): + import inspect + fn = py.path.local(inspect.getfile(function)) + lineno = py.builtin._getcode(function).co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" %(fn, lineno+1) diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index 629498e26..b4ed48528 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -8,7 +8,7 @@ from py._plugin import hookspec default_plugins = ( "default runner pdb capture mark terminal skipping tmpdir monkeypatch " "recwarn pastebin unittest helpconfig nose assertion genscript " - "junitxml doctest").split() + "junitxml doctest keyword").split() def check_old_use(mod, modname): clsname = modname[len('pytest_'):].capitalize() + "Plugin" diff --git a/py/_test/pycollect.py b/py/_test/pycollect.py index 720e2f1c7..e4514fc31 100644 --- a/py/_test/pycollect.py +++ b/py/_test/pycollect.py @@ -348,8 +348,9 @@ class Function(FunctionMixin, py.test.collect.Item): """ _genid = None def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy): - super(Function, self).__init__(name, parent, config=config) + callspec=None, callobj=_dummy, collection=None): + super(Function, self).__init__(name, parent, + config=config, collection=collection) self._args = args if self._isyieldedfunction(): assert not callspec, "yielded functions (deprecated) cannot have funcargs" diff --git a/py/_test/session.py b/py/_test/session.py index 64d724d13..d65a8ab30 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -6,6 +6,28 @@ """ import py +import sys + +# +# main entry point +# + +def main(args=None): + if args is None: + args = sys.argv[1:] + config = py.test.config + try: + config.parse(args) + config.pluginmanager.do_configure(config) + exitstatus = config.hook.pytest_cmdline_main(config=config) + config.pluginmanager.do_unconfigure(config) + except config.Error: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = EXIT_INTERNALERROR + py.test.config = py.test.config.__class__() + return exitstatus + # exitcodes for the command line EXIT_OK = 0 @@ -24,65 +46,13 @@ class Session(object): """ signals an interrupted test run. """ __module__ = 'builtins' # for py3 - def __init__(self, config): + def __init__(self, config, collection): self.config = config self.pluginmanager = config.pluginmanager # shortcut self.pluginmanager.register(self) self._testsfailed = 0 - self._nomatch = False self.shouldstop = False - - def genitems(self, colitems, keywordexpr=None): - """ yield Items from iterating over the given colitems. """ - if colitems: - colitems = list(colitems) - while colitems: - next = colitems.pop(0) - if isinstance(next, (tuple, list)): - colitems[:] = list(next) + colitems - continue - assert self.pluginmanager is next.config.pluginmanager - if isinstance(next, Item): - remaining = self.filteritems([next]) - if remaining: - self.config.hook.pytest_itemstart(item=next) - yield next - else: - assert isinstance(next, Collector) - self.config.hook.pytest_collectstart(collector=next) - rep = self.config.hook.pytest_make_collect_report(collector=next) - if rep.passed: - for x in self.genitems(rep.result, keywordexpr): - yield x - self.config.hook.pytest_collectreport(report=rep) - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def filteritems(self, colitems): - """ return items to process (some may be deselected)""" - keywordexpr = self.config.option.keyword - if not keywordexpr or self._nomatch: - return colitems - if keywordexpr[-1] == ":": - keywordexpr = keywordexpr[:-1] - remaining = [] - deselected = [] - for colitem in colitems: - if isinstance(colitem, Item): - if colitem._skipbykeyword(keywordexpr): - deselected.append(colitem) - continue - remaining.append(colitem) - if deselected: - self.config.hook.pytest_deselected(items=deselected) - if self.config.option.keyword.endswith(":"): - self._nomatch = True - return remaining - - def collect(self, colitems): - keyword = self.config.option.keyword - for x in self.genitems(colitems, keyword): - yield x + self.collection = collection def sessionstarts(self): """ setup any neccessary resources ahead of the test run. """ @@ -95,6 +65,7 @@ class Session(object): if maxfail and self._testsfailed >= maxfail: self.shouldstop = "stopping after %d failures" % ( self._testsfailed) + self.collection.shouldstop = self.shouldstop pytest_collectreport = pytest_runtest_logreport def sessionfinishes(self, exitstatus): @@ -104,13 +75,14 @@ class Session(object): exitstatus=exitstatus, ) - def main(self, colitems): + def main(self): """ main loop for running tests. """ self.shouldstop = False + self.sessionstarts() exitstatus = EXIT_OK try: - self._mainloop(colitems) + self._mainloop() if self._testsfailed: exitstatus = EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus) @@ -126,10 +98,165 @@ class Session(object): self.sessionfinishes(exitstatus=exitstatus) return exitstatus - def _mainloop(self, colitems): - for item in self.collect(colitems): - if not self.config.option.collectonly: - item.config.hook.pytest_runtest_protocol(item=item) + def _mainloop(self): + if self.config.option.collectonly: + return + for item in self.collection.items: + item.config.hook.pytest_runtest_protocol(item=item) if self.shouldstop: raise self.Interrupted(self.shouldstop) + +class Collection: + def __init__(self, config): + self.config = config + self.topdir = gettopdir(self.config.args) + self._argfspaths = [py.path.local(decodearg(x)[0]) + for x in self.config.args] + x = py.test.collect.Directory(fspath=self.topdir, + config=config, collection=self) + self._topcollector = x.consider_dir(self.topdir) + self._topcollector.parent = None + + def _normalizearg(self, arg): + return "::".join(self._parsearg(arg)) + + def _parsearg(self, arg): + """ return normalized name list for a command line specified id + which might be of the form x/y/z::name1::name2 + and should result into the form x::y::z::name1::name2 + """ + parts = str(arg).split("::") + path = py.path.local(parts[0]) + if not path.check(): + raise self.config.Error("file not found: %s" %(path,)) + topdir = self.topdir + if path != topdir and not path.relto(topdir): + raise self.config.Error("path %r is not relative to %r" % + (str(path), str(topdir))) + topparts = path.relto(topdir).split(path.sep) + return topparts + parts[1:] + + def getid(self, node, relative=True): + """ return id for node, relative to topdir. """ + path = node.fspath + chain = [x for x in node.listchain() if x.fspath == path] + chain = chain[1:] + names = [x.name for x in chain if x.name != "()"] + if relative: + relpath = path.relto(self.topdir) + if relpath: + path = relpath + names = relpath.split(node.fspath.sep) + names + return "::".join(names) + + def getbyid(self, id): + """ return one or more nodes matching the id. """ + matching = [self._topcollector] + if not id: + return matching + names = id.split("::") + while names: + name = names.pop(0) + l = [] + for current in matching: + for x in current._memocollect(): + if x.name == name: + l.append(x) + elif x.name == "()": + names.insert(0, name) + l.append(x) + break + if not l: + raise ValueError("no node named %r below %r" %(name, current)) + matching = l + return matching + + def do_collection(self): + assert not hasattr(self, 'items') + hook = self.config.hook + hook.pytest_log_startcollection(collection=self) + try: + self.items = self.perform_collect() + except self.config.Error: + raise + except Exception: + self.config.pluginmanager.notify_exception() + return EXIT_INTERNALERROR + else: + hook.pytest_collection_modifyitems(collection=self) + res = hook.pytest_log_finishcollection(collection=self) + return res and max(res) or 0 # returncode + + def getinitialnodes(self): + idlist = [self._normalizearg(arg) for arg in self.config.args] + nodes = [] + for id in idlist: + nodes.extend(self.getbyid(id)) + return nodes + + def perform_collect(self): + idlist = [self._parsearg(arg) for arg in self.config.args] + nodes = [] + for names in idlist: + self.genitems([self._topcollector], names, nodes) + return nodes + + def genitems(self, matching, names, result): + names = list(names) + name = names and names.pop(0) or None + for node in matching: + if isinstance(node, Item): + if name is None: + self.config.hook.pytest_log_itemcollect(item=node) + result.append(node) + else: + assert isinstance(node, Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + #print "matching", rep.result, "against name", name + if rep.passed: + if name: + matched = False + for subcol in rep.result: + if subcol.name != name and subcol.name == "()": + names.insert(0, name) + name = "()" + # see doctests/custom naming XXX + if subcol.name == name or subcol.fspath.basename == name: + self.genitems([subcol], names, result) + matched = True + if not matched: + raise self.config.Error( + "can't collect: %s" % (name,)) + + else: + self.genitems(rep.result, [], result) + node.ihook.pytest_collectreport(report=rep) + x = getattr(self, 'shouldstop', None) + if x: + raise self.Interrupted(x) + +def gettopdir(args): + """ return the top directory for the given paths. + if the common base dir resides in a python package + parent directory of the root package is returned. + """ + fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] + p = fsargs and fsargs[0] or None + for x in fsargs[1:]: + p = p.common(x) + assert p, "cannot determine common basedir of %s" %(fsargs,) + pkgdir = p.pypkgpath() + if pkgdir is None: + if p.check(file=1): + p = p.dirpath() + return p + else: + return pkgdir.dirpath() + +def decodearg(arg): + arg = str(arg) + return arg.split("::") + + diff --git a/setup.py b/setup.py index 0cde55ed6..49c311505 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): name='py', description='py.test and pylib: rapid testing and development utils.', long_description = long_description, - version= '1.3.4', + version= '1.4.0a1', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 5b2940d65..faf27ea12 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -72,7 +72,7 @@ class TestGeneralUsage: result = testdir.runpytest(p1, p2) assert result.ret != 0 result.stderr.fnmatch_lines([ - "*ERROR: can't collect: %s" %(p2,) + "*ERROR: can't collect:*%s" %(p2.basename,) ]) @@ -122,7 +122,6 @@ class TestGeneralUsage: ]) - @py.test.mark.xfail def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" import py @@ -163,3 +162,32 @@ class TestGeneralUsage: """)) result = testdir.runpython(p, prepend=False) assert not result.ret + + @py.test.mark.xfail(reason="http://bitbucket.org/hpk42/py-trunk/issue/109") + def test_sibling_conftest_issue109(self, testdir): + """ + This test is to make sure that the conftest.py of sibling directories is not loaded + if py.test is run for/in one of the siblings directory and those sibling directories + are not packaged together with an __init__.py. See bitbucket issue #109. + """ + for dirname in ['a', 'b']: + testdir.tmpdir.ensure(dirname, dir=True) + testdir.tmpdir.ensure(dirname, '__init__.py') + + # To create the conftest.py I would like to use testdir.make*-methods + # but as far as I have seen they can only create files in testdir.tempdir + # Maybe there is a way to explicitly specifiy the directory on which those + # methods work or a completely better way to do that? + backupTmpDir = testdir.tmpdir + testdir.tmpdir = testdir.tmpdir.join(dirname) + testdir.makeconftest(""" + _DIR_NAME = '%s' + def pytest_configure(config): + if config.args and config.args[0] != _DIR_NAME: + raise Exception("py.test run for '" + config.args[0] + "', but '" + _DIR_NAME + "/conftest.py' loaded.") + """ % dirname) + testdir.tmpdir = backupTmpDir + + for dirname, other_dirname in [('a', 'b'), ('b', 'a')]: + result = testdir.runpytest(dirname) + assert result.ret == 0, "test_sibling_conftest: py.test run for '%s', but '%s/conftest.py' loaded." % (dirname, other_dirname) diff --git a/testing/plugin/test_pytest_genscript.py b/testing/plugin/test_pytest_genscript.py index c421460fc..a880f3904 100644 --- a/testing/plugin/test_pytest_genscript.py +++ b/testing/plugin/test_pytest_genscript.py @@ -26,6 +26,7 @@ def test_gen(testdir, anypython, standalone): "*imported from*mypytest" ]) +@py.test.mark.xfail(reason="fix-dist") def test_rundist(testdir, pytestconfig, standalone): pytestconfig.pluginmanager.skipifmissing("xdist") testdir.makepyfile(""" diff --git a/testing/test_genitems.py b/testing/plugin/test_pytest_keyword.py similarity index 100% rename from testing/test_genitems.py rename to testing/plugin/test_pytest_keyword.py diff --git a/testing/plugin/test_pytest_pytester.py b/testing/plugin/test_pytest_pytester.py index a6a968dfe..1e1c3bf9b 100644 --- a/testing/plugin/test_pytest_pytester.py +++ b/testing/plugin/test_pytest_pytester.py @@ -5,6 +5,8 @@ def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") recorder = testdir.getreportrecorder(item.config) assert not recorder.getfailures() + + py.test.xfail("internal reportrecorder tests need refactoring") class rep: excinfo = None passed = False diff --git a/testing/plugin/test_pytest_resultlog.py b/testing/plugin/test_pytest_resultlog.py index 1ed512140..8171441c6 100644 --- a/testing/plugin/test_pytest_resultlog.py +++ b/testing/plugin/test_pytest_resultlog.py @@ -3,10 +3,12 @@ import os from py._plugin.pytest_resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure from py._test.collect import Node, Item, FSCollector +from py._test.session import Collection def test_generic_path(testdir): config = testdir.parseconfig() - p1 = Node('a', parent=config._rootcol) + collection = Collection(config) + p1 = Node('a', config=config, collection=collection) #assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -15,7 +17,7 @@ def test_generic_path(testdir): res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test', parent=config._rootcol) + p0 = FSCollector('proj/test', config=config, collection=collection) p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) diff --git a/testing/plugin/test_pytest_skipping.py b/testing/plugin/test_pytest_skipping.py index 7f8f8adb1..9f0958d37 100644 --- a/testing/plugin/test_pytest_skipping.py +++ b/testing/plugin/test_pytest_skipping.py @@ -408,8 +408,8 @@ def test_skipped_reasons_functional(testdir): ) result = testdir.runpytest('--report=skipped') result.stdout.fnmatch_lines([ - "*test_one.py ss", "*test_two.py S", + "*test_one.py ss", "*SKIP*3*conftest.py:3: 'test'", ]) assert result.ret == 0 diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index 28a522a32..dd63826ae 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -206,7 +206,7 @@ class TestCollectonly: "" ]) item = modcol.join("test_func") - rep.config.hook.pytest_itemstart(item=item) + rep.config.hook.pytest_log_itemcollect(item=item) linecomp.assert_contains_lines([ " ", ]) @@ -264,13 +264,13 @@ class TestCollectonly: stderr = result.stderr.str().strip() #assert stderr.startswith("inserting into sys.path") assert result.ret == 0 - extra = result.stdout.fnmatch_lines(py.code.Source(""" - - - - - - """).strip()) + extra = result.stdout.fnmatch_lines([ + "*", + "* ", + "* ", + "* ", + "* ", + ]) def test_collectonly_error(self, testdir): p = testdir.makepyfile("import Errlkjqweqwe") @@ -278,9 +278,9 @@ class TestCollectonly: stderr = result.stderr.str().strip() assert result.ret == 1 extra = result.stdout.fnmatch_lines(py.code.Source(""" - + * *ImportError* - !!!*failures*!!! + *!!!*failures*!!! *test_collectonly_error.py:1* """).strip()) @@ -454,6 +454,7 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 + py.test.xfail("fix dist-testing") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ diff --git a/testing/test_collect.py b/testing/test_collect.py index 43e611f53..82b3b5cdd 100644 --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -59,19 +59,18 @@ class TestCollector: import py class CustomFile(py.test.collect.File): pass - class MyDirectory(py.test.collect.Directory): - def collect(self): - return [CustomFile(self.fspath.join("hello.xxx"), parent=self)] - def pytest_collect_directory(path, parent): - return MyDirectory(path, parent=parent) + def pytest_collect_file(path, parent): + if path.ext == ".xxx": + return CustomFile(path, parent=parent) """) config = testdir.parseconfig(hello) - node = config.getnode(hello) + node = testdir.getnode(config, hello) assert isinstance(node, py.test.collect.File) assert node.name == "hello.xxx" - names = config._rootcol.totrail(node) - node = config._rootcol.getbynames(names) - assert isinstance(node, py.test.collect.File) + id = node.collection.getid(node) + nodes = node.collection.getbyid(id) + assert len(nodes) == 1 + assert isinstance(nodes[0], py.test.collect.File) class TestCollectFS: def test_ignored_certain_directories(self, testdir): @@ -84,7 +83,7 @@ class TestCollectFS: tmpdir.ensure("normal", 'test_found.py') tmpdir.ensure('test_found.py') - col = testdir.parseconfig(tmpdir).getnode(tmpdir) + col = testdir.getnode(testdir.parseconfig(tmpdir), tmpdir) items = col.collect() names = [x.name for x in items] assert len(items) == 2 @@ -93,7 +92,7 @@ class TestCollectFS: def test_found_certain_testfiles(self, testdir): p1 = testdir.makepyfile(test_found = "pass", found_test="pass") - col = testdir.parseconfig(p1).getnode(p1.dirpath()) + col = testdir.getnode(testdir.parseconfig(p1), p1.dirpath()) items = col.collect() # Directory collect returns files sorted by name assert len(items) == 2 assert items[1].name == 'test_found.py' @@ -106,7 +105,7 @@ class TestCollectFS: testdir.makepyfile(test_two="hello") p1.dirpath().mkdir("dir2") config = testdir.parseconfig() - col = config.getnode(p1.dirpath()) + col = testdir.getnode(config, p1.dirpath()) names = [x.name for x in col.collect()] assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] @@ -120,7 +119,7 @@ class TestCollectPluginHookRelay: config = testdir.Config() config.pluginmanager.register(Plugin()) config.parse([tmpdir]) - col = config.getnode(tmpdir) + col = testdir.getnode(config, tmpdir) testdir.makefile(".abc", "xyz") res = col.collect() assert len(wascalled) == 1 @@ -236,7 +235,7 @@ class TestCustomConftests: assert 'test_world.py' in names def test_pytest_fs_collect_hooks_are_seen(self, testdir): - testdir.makeconftest(""" + conf = testdir.makeconftest(""" import py class MyDirectory(py.test.collect.Directory): pass @@ -247,79 +246,11 @@ class TestCustomConftests: def pytest_collect_file(path, parent): return MyModule(path, parent) """) - testdir.makepyfile("def test_x(): pass") + sub = testdir.mkdir("sub") + p = testdir.makepyfile("def test_x(): pass") result = testdir.runpytest("--collectonly") result.stdout.fnmatch_lines([ "*MyDirectory*", "*MyModule*", "*test_x*" ]) - -class TestRootCol: - def test_totrail_and_back(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - tmpdir.ensure("a", "__init__.py") - x = tmpdir.ensure("a", "trail.py") - config = testdir.reparseconfig([x]) - col = config.getnode(x) - trail = config._rootcol.totrail(col) - col2 = config._rootcol.fromtrail(trail) - assert col2 == col - - @py.test.mark.xfail(reason="http://bitbucket.org/hpk42/py-trunk/issue/109") - def test_sibling_conftest_issue109(self, testdir): - """ - This test is to make sure that the conftest.py of sibling directories is not loaded - if py.test is run for/in one of the siblings directory and those sibling directories - are not packaged together with an __init__.py. See bitbucket issue #109. - """ - for dirname in ['a', 'b']: - testdir.tmpdir.ensure(dirname, dir=True) - testdir.tmpdir.ensure(dirname, '__init__.py') - - # To create the conftest.py I would like to use testdir.make*-methods - # but as far as I have seen they can only create files in testdir.tempdir - # Maybe there is a way to explicitly specifiy the directory on which those - # methods work or a completely better way to do that? - backupTmpDir = testdir.tmpdir - testdir.tmpdir = testdir.tmpdir.join(dirname) - testdir.makeconftest(""" - _DIR_NAME = '%s' - def pytest_configure(config): - if config.args and config.args[0] != _DIR_NAME: - raise Exception("py.test run for '" + config.args[0] + "', but '" + _DIR_NAME + "/conftest.py' loaded.") - """ % dirname) - testdir.tmpdir = backupTmpDir - - for dirname, other_dirname in [('a', 'b'), ('b', 'a')]: - result = testdir.runpytest(dirname) - assert result.ret == 0, "test_sibling_conftest: py.test run for '%s', but '%s/conftest.py' loaded." % (dirname, other_dirname) - - def test_totrail_topdir_and_beyond(self, testdir, tmpdir): - config = testdir.reparseconfig() - col = config.getnode(config.topdir) - trail = config._rootcol.totrail(col) - col2 = config._rootcol.fromtrail(trail) - assert col2.fspath == config.topdir - assert len(col2.listchain()) == 1 - py.test.raises(config.Error, "config.getnode(config.topdir.dirpath())") - #col3 = config.getnode(config.topdir.dirpath()) - #py.test.raises(ValueError, - # "col3._totrail()") - - def test_argid(self, testdir, tmpdir): - cfg = testdir.parseconfig() - p = testdir.makepyfile("def test_func(): pass") - item = cfg.getnode("%s::test_func" % p) - assert item.name == "test_func" - - def test_argid_with_method(self, testdir, tmpdir): - cfg = testdir.parseconfig() - p = testdir.makepyfile(""" - class TestClass: - def test_method(self): pass - """) - item = cfg.getnode("%s::TestClass::()::test_method" % p) - assert item.name == "test_method" - item = cfg.getnode("%s::TestClass::test_method" % p) - assert item.name == "test_method" diff --git a/testing/test_collection.py b/testing/test_collection.py new file mode 100644 index 000000000..255b8a16c --- /dev/null +++ b/testing/test_collection.py @@ -0,0 +1,314 @@ +import py + +from py._test.session import Collection, gettopdir + +class TestCollection: + def test_parsearg(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + subdir = testdir.mkdir("sub") + subdir.ensure("__init__.py") + target = subdir.join(p.basename) + p.move(target) + testdir.chdir() + subdir.chdir() + config = testdir.parseconfig(p.basename) + rcol = Collection(config=config) + assert rcol.topdir == testdir.tmpdir + parts = rcol._parsearg(p.basename) + assert parts[0] == "sub" + assert parts[1] == p.basename + assert len(parts) == 2 + parts = rcol._parsearg(p.basename + "::test_func") + assert parts[0] == "sub" + assert parts[1] == p.basename + assert parts[2] == "test_func" + assert len(parts) == 3 + + def test_collect_topdir(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + id = "::".join([p.basename, "test_func"]) + config = testdir.parseconfig(id) + topdir = testdir.tmpdir + rcol = Collection(config) + assert topdir == rcol.topdir + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + root = items[0].listchain()[0] + root_id = rcol.getid(root) + root2 = rcol.getbyid(root_id)[0] + assert root2.fspath == root.fspath + + def test_collect_protocol_single_function(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + id = "::".join([p.basename, "test_func"]) + config = testdir.parseconfig(id) + topdir = testdir.tmpdir + rcol = Collection(config) + assert topdir == rcol.topdir + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + item = items[0] + assert item.name == "test_func" + newid = rcol.getid(item) + assert newid == id + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == topdir"), + ("pytest_make_collect_report", "collector.fspath == topdir"), + ("pytest_collectstart", "collector.fspath == p"), + ("pytest_make_collect_report", "collector.fspath == p"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == p"), + ("pytest_collectreport", "report.collector.fspath == topdir") + ]) + + def test_collect_protocol_method(self, testdir): + p = testdir.makepyfile(""" + class TestClass: + def test_method(self): + pass + """) + normid = p.basename + "::TestClass::test_method" + for id in [p.basename, + p.basename + "::TestClass", + p.basename + "::TestClass::()", + p.basename + "::TestClass::()::test_method", + normid, + ]: + config = testdir.parseconfig(id) + rcol = Collection(config=config) + nodes = rcol.perform_collect() + assert len(nodes) == 1 + assert nodes[0].name == "test_method" + newid = rcol.getid(nodes[0]) + assert newid == normid + + def test_collect_custom_nodes_multi_id(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + testdir.makeconftest(""" + import py + class SpecialItem(py.test.collect.Item): + def runtest(self): + return # ok + class SpecialFile(py.test.collect.File): + def collect(self): + return [SpecialItem(name="check", parent=self)] + def pytest_collect_file(path, parent): + if path.basename == %r: + return SpecialFile(fspath=path, parent=parent) + """ % p.basename) + id = p.basename + + config = testdir.parseconfig(id) + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + py.std.pprint.pprint(hookrec.hookrecorder.calls) + assert len(items) == 2 + hookrec.hookrecorder.contains([ + ("pytest_collectstart", + "collector.fspath == collector.collection.topdir"), + ("pytest_collectstart", + "collector.__class__.__name__ == 'SpecialFile'"), + ("pytest_collectstart", + "collector.__class__.__name__ == 'Module'"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == p"), + ("pytest_collectreport", + "report.collector.fspath == report.collector.collection.topdir") + ]) + + def test_collect_subdir_event_ordering(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + aaa = testdir.mkpydir("aaa") + test_aaa = aaa.join("test_aaa.py") + p.move(test_aaa) + config = testdir.parseconfig() + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 1 + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == test_aaa"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == test_aaa"), + ("pytest_collectreport", "report.collector.fspath == aaa"), + ]) + + def test_collect_two_commandline_args(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + aaa = testdir.mkpydir("aaa") + bbb = testdir.mkpydir("bbb") + p.copy(aaa.join("test_aaa.py")) + p.move(bbb.join("test_bbb.py")) + + id = "." + config = testdir.parseconfig(id) + rcol = Collection(config) + hookrec = testdir.getreportrecorder(config) + items = rcol.perform_collect() + assert len(items) == 2 + py.std.pprint.pprint(hookrec.hookrecorder.calls) + hookrec.hookrecorder.contains([ + ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_pycollect_makeitem", "name == 'test_func'"), + ("pytest_collectreport", "report.collector.fspath == bbb"), + ]) + + def test_serialization_byid(self, testdir): + p = testdir.makepyfile("def test_func(): pass") + config = testdir.parseconfig() + rcol = Collection(config) + items = rcol.perform_collect() + assert len(items) == 1 + item, = items + id = rcol.getid(item) + newcol = Collection(config) + item2, = newcol.getbyid(id) + assert item2.name == item.name + assert item2.fspath == item.fspath + item2b, = newcol.getbyid(id) + assert item2b is item2 + +class Test_gettopdir: + def test_gettopdir(self, testdir): + tmp = testdir.tmpdir + assert gettopdir([tmp]) == tmp + topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) + assert topdir == tmp + somefile = tmp.ensure("somefile.py") + assert gettopdir([somefile]) == tmp + + def test_gettopdir_pypkg(self, testdir): + tmp = testdir.tmpdir + a = tmp.ensure('a', dir=1) + b = tmp.ensure('a', 'b', '__init__.py') + c = tmp.ensure('a', 'b', 'c.py') + Z = tmp.ensure('Z', dir=1) + assert gettopdir([c]) == a + assert gettopdir([c, Z]) == tmp + assert gettopdir(["%s::xyc" % c]) == a + assert gettopdir(["%s::xyc::abc" % c]) == a + assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp + +class Test_getinitialnodes: + def test_onedir(self, testdir): + config = testdir.reparseconfig([testdir.tmpdir]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 1 + col = colitems[0] + assert isinstance(col, py.test.collect.Directory) + for col in col.listchain(): + assert col.config is config + + def test_twodirs(self, testdir, tmpdir): + config = testdir.reparseconfig([tmpdir, tmpdir]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == col2.name + assert col1.parent == col2.parent + + def test_curdir_and_subdir(self, testdir, tmpdir): + a = tmpdir.ensure("a", dir=1) + config = testdir.reparseconfig([tmpdir, a]) + colitems = Collection(config).getinitialnodes() + assert len(colitems) == 2 + col1, col2 = colitems + assert col1.name == tmpdir.basename + assert col2.name == 'a' + for col in colitems: + for subcol in col.listchain(): + assert col.config is config + + def test_global_file(self, testdir, tmpdir): + x = tmpdir.ensure("x.py") + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == tmpdir.basename + assert col.parent.parent is None + for col in col.listchain(): + assert col.config is config + + def test_global_dir(self, testdir, tmpdir): + x = tmpdir.ensure("a", dir=1) + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Directory) + print(col.listchain()) + assert col.name == 'a' + assert col.parent is None + assert col.config is config + + def test_pkgfile(self, testdir, tmpdir): + tmpdir = tmpdir.join("subdir") + x = tmpdir.ensure("x.py") + tmpdir.ensure("__init__.py") + config = testdir.reparseconfig([x]) + col, = Collection(config).getinitialnodes() + assert isinstance(col, py.test.collect.Module) + assert col.name == 'x.py' + assert col.parent.name == x.dirpath().basename + assert col.parent.parent.parent is None + for col in col.listchain(): + assert col.config is config + +class Test_genitems: + def test_check_collect_hashes(self, testdir): + p = testdir.makepyfile(""" + def test_1(): + pass + + def test_2(): + pass + """) + p.copy(p.dirpath(p.purebasename + "2" + ".py")) + items, reprec = testdir.inline_genitems(p.dirpath()) + assert len(items) == 4 + for numi, i in enumerate(items): + for numj, j in enumerate(items): + if numj != numi: + assert hash(i) != hash(j) + assert i != j + + def test_root_conftest_syntax_error(self, testdir): + # do we want to unify behaviour with + # test_subdir_conftest_error? + p = testdir.makepyfile(conftest="raise SyntaxError\n") + py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + + def test_example_items1(self, testdir): + p = testdir.makepyfile(''' + def testone(): + pass + + class TestX: + def testmethod_one(self): + pass + + class TestY(TestX): + pass + ''') + items, reprec = testdir.inline_genitems(p) + assert len(items) == 3 + assert items[0].name == 'testone' + assert items[1].name == 'testmethod_one' + assert items[2].name == 'testmethod_one' + + # let's also test getmodpath here + assert items[0].getmodpath() == "testone" + assert items[1].getmodpath() == "TestX.testmethod_one" + assert items[2].getmodpath() == "TestY.testmethod_one" + + s = items[0].getmodpath(stopatmodule=False) + assert s.endswith("test_example_items1.testone") + print(s) diff --git a/testing/test_config.py b/testing/test_config.py index c80aae237..2c839866a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,6 +1,4 @@ import py -from py._test.collect import RootCollector - class TestConfigCmdlineParsing: def test_parser_addoption_default_env(self, testdir, monkeypatch): @@ -106,104 +104,6 @@ class TestConfigAPI: assert pl[0] == tmpdir assert pl[1] == somepath - def test_setsessionclass_and_initsession(self, testdir): - config = testdir.Config() - class Session1: - def __init__(self, config): - self.config = config - config.setsessionclass(Session1) - session = config.initsession() - assert isinstance(session, Session1) - assert session.config is config - py.test.raises(ValueError, "config.setsessionclass(Session1)") - - -class TestConfigApi_getinitialnodes: - def test_onedir(self, testdir): - config = testdir.reparseconfig([testdir.tmpdir]) - colitems = config.getinitialnodes() - assert len(colitems) == 1 - col = colitems[0] - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col.config is config - - def test_twodirs(self, testdir, tmpdir): - config = testdir.reparseconfig([tmpdir, tmpdir]) - colitems = config.getinitialnodes() - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == col2.name - assert col1.parent == col2.parent - - def test_curdir_and_subdir(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([tmpdir, a]) - colitems = config.getinitialnodes() - assert len(colitems) == 2 - col1, col2 = colitems - assert col1.name == tmpdir.basename - assert col2.name == 'a' - for col in colitems: - for subcol in col.listchain(): - assert col.config is config - - def test_global_file(self, testdir, tmpdir): - x = tmpdir.ensure("x.py") - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == tmpdir.basename - assert isinstance(col.parent.parent, RootCollector) - for col in col.listchain(): - assert col.config is config - - def test_global_dir(self, testdir, tmpdir): - x = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Directory) - print(col.listchain()) - assert col.name == 'a' - assert isinstance(col.parent, RootCollector) - assert col.config is config - - def test_pkgfile(self, testdir, tmpdir): - tmpdir = tmpdir.join("subdir") - x = tmpdir.ensure("x.py") - tmpdir.ensure("__init__.py") - config = testdir.reparseconfig([x]) - col, = config.getinitialnodes() - assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert isinstance(col.parent.parent.parent, RootCollector) - for col in col.listchain(): - assert col.config is config - -class TestConfig_gettopdir: - def test_gettopdir(self, testdir): - from py._test.config import gettopdir - tmp = testdir.tmpdir - assert gettopdir([tmp]) == tmp - topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) - assert topdir == tmp - somefile = tmp.ensure("somefile.py") - assert gettopdir([somefile]) == tmp - - def test_gettopdir_pypkg(self, testdir): - from py._test.config import gettopdir - tmp = testdir.tmpdir - a = tmp.ensure('a', dir=1) - b = tmp.ensure('a', 'b', '__init__.py') - c = tmp.ensure('a', 'b', 'c.py') - Z = tmp.ensure('Z', dir=1) - assert gettopdir([c]) == a - assert gettopdir([c, Z]) == tmp - assert gettopdir(["%s::xyc" % c]) == a - assert gettopdir(["%s::xyc::abc" % c]) == a - assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): @@ -247,133 +147,3 @@ def test_preparse_ordering(testdir, monkeypatch): config = testdir.parseconfig() plugin = config.pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 - - -import pickle -class TestConfigPickling: - def pytest_funcarg__testdir(self, request): - oldconfig = py.test.config - print("setting py.test.config to None") - py.test.config = None - def resetglobals(): - py.builtin.print_("setting py.test.config to", oldconfig) - py.test.config = oldconfig - request.addfinalizer(resetglobals) - return request.getfuncargvalue("testdir") - - def test_config_getstate_setstate(self, testdir): - from py._test.config import Config - testdir.makepyfile(__init__="", conftest="x=1; y=2") - hello = testdir.makepyfile(hello="") - tmp = testdir.tmpdir - testdir.chdir() - config1 = testdir.parseconfig(hello) - config2 = Config() - config2.__setstate__(config1.__getstate__()) - assert config2.topdir == py.path.local() - config2_relpaths = [py.path.local(x).relto(config2.topdir) - for x in config2.args] - config1_relpaths = [py.path.local(x).relto(config1.topdir) - for x in config1.args] - - assert config2_relpaths == config1_relpaths - for name, value in config1.option.__dict__.items(): - assert getattr(config2.option, name) == value - assert config2.getvalue("x") == 1 - - def test_config_pickling_customoption(self, testdir): - testdir.makeconftest(""" - def pytest_addoption(parser): - group = parser.getgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") - """) - config = testdir.parseconfig("-G", "11") - assert config.option.gdest == 11 - repr = config.__getstate__() - - config = testdir.Config() - py.test.raises(AttributeError, "config.option.gdest") - - config2 = testdir.Config() - config2.__setstate__(repr) - assert config2.option.gdest == 11 - - def test_config_pickling_and_conftest_deprecated(self, testdir): - tmp = testdir.tmpdir.ensure("w1", "w2", dir=1) - tmp.ensure("__init__.py") - tmp.join("conftest.py").write(py.code.Source(""" - def pytest_addoption(parser): - group = parser.getgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") - """)) - config = testdir.parseconfig(tmp, "-G", "11") - assert config.option.gdest == 11 - repr = config.__getstate__() - - config = testdir.Config() - py.test.raises(AttributeError, "config.option.gdest") - - config2 = testdir.Config() - config2.__setstate__(repr) - assert config2.option.gdest == 11 - - option = config2.addoptions("testing group", - config2.Option('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.")) - assert option.gdest == 11 - - def test_config_picklability(self, testdir): - config = testdir.parseconfig() - s = pickle.dumps(config) - newconfig = pickle.loads(s) - assert hasattr(newconfig, "topdir") - assert newconfig.topdir == py.path.local() - - def test_collector_implicit_config_pickling(self, testdir): - tmpdir = testdir.tmpdir - testdir.chdir() - testdir.makepyfile(hello="def test_x(): pass") - config = testdir.parseconfig(tmpdir) - col = config.getnode(config.topdir) - io = py.io.BytesIO() - pickler = pickle.Pickler(io) - pickler.dump(col) - io.seek(0) - unpickler = pickle.Unpickler(io) - col2 = unpickler.load() - assert col2.name == col.name - assert col2.listnames() == col.listnames() - - def test_config_and_collector_pickling(self, testdir): - tmpdir = testdir.tmpdir - dir1 = tmpdir.ensure("sourcedir", "somedir", dir=1) - config = testdir.parseconfig() - assert config.topdir == tmpdir - col = config.getnode(dir1.dirpath()) - col1 = config.getnode(dir1) - assert col1.parent == col - io = py.io.BytesIO() - pickler = pickle.Pickler(io) - pickler.dump(col) - pickler.dump(col1) - pickler.dump(col) - io.seek(0) - unpickler = pickle.Unpickler(io) - newtopdir = tmpdir.ensure("newtopdir", dir=1) - newtopdir.mkdir("sourcedir").mkdir("somedir") - old = newtopdir.chdir() - try: - newcol = unpickler.load() - newcol2 = unpickler.load() - newcol3 = unpickler.load() - assert newcol2.config is newcol.config - assert newcol2.parent == newcol - assert newcol2.config.topdir.realpath() == newtopdir.realpath() - newsourcedir = newtopdir.join("sourcedir") - assert newcol.fspath.realpath() == newsourcedir.realpath() - assert newcol2.fspath.basename == dir1.basename - assert newcol2.fspath.relto(newcol2.config.topdir) - finally: - old.chdir() diff --git a/testing/test_conftesthandle.py b/testing/test_conftesthandle.py index afa59d572..b6a35902e 100644 --- a/testing/test_conftesthandle.py +++ b/testing/test_conftesthandle.py @@ -82,10 +82,10 @@ class TestConftestValueAccessGlobal: #conftest.lget("b") == 1 def test_value_access_with_confmod(self, basedir): - topdir = basedir.join("adir", "b") - topdir.ensure("xx", dir=True) - conftest = ConftestWithSetinitial(topdir) - mod, value = conftest.rget_with_confmod("a", topdir) + startdir = basedir.join("adir", "b") + startdir.ensure("xx", dir=True) + conftest = ConftestWithSetinitial(startdir) + mod, value = conftest.rget_with_confmod("a", startdir) assert value == 1.5 path = py.path.local(mod.__file__) assert path.dirpath() == basedir.join("adir", "b") diff --git a/testing/test_deprecated_api.py b/testing/test_deprecated_api.py index 4f4757132..67a1c6700 100644 --- a/testing/test_deprecated_api.py +++ b/testing/test_deprecated_api.py @@ -49,7 +49,7 @@ class TestCollectDeprecated: def check2(self): pass """)) config = testdir.parseconfig(somefile) - dirnode = config.getnode(somefile.dirpath()) + dirnode = testdir.getnode(config, somefile.dirpath()) colitems = dirnode.collect() w = recwarn.pop(DeprecationWarning) assert w.filename.find("conftest.py") != -1 @@ -170,10 +170,13 @@ class TestCollectDeprecated: if path.basename == "testme.xxx": return Module(path, parent=self) return super(Directory, self).consider_file(path) + #def pytest_collect_file(path, parent): + # if path.basename == "testme.xxx": + # return Module(path, parent=parent) """) testme = testdir.makefile('xxx', testme="hello") config = testdir.parseconfig(testme) - col = config.getnode(testme) + col = testdir.getnode(config, testme) assert col.collect() == [] @@ -219,7 +222,7 @@ class TestDisabled: """) reprec.assertoutcome(skipped=2) - @py.test.mark.multi(name="Directory Module Class Function".split()) + @py.test.mark.multi(name="Module Class Function".split()) def test_function_deprecated_run_execute(self, name, testdir, recwarn): testdir.makeconftest(""" import py @@ -235,11 +238,11 @@ class TestDisabled: """) config = testdir.parseconfig() if name == "Directory": - config.getnode(testdir.tmpdir) + testdir.getnode(config, testdir.tmpdir) elif name in ("Module", "File"): - config.getnode(p) + testdir.getnode(config, p) else: - fnode = config.getnode(p) + fnode = testdir.getnode(config, p) recwarn.clear() fnode.collect() w = recwarn.pop(DeprecationWarning) @@ -278,9 +281,10 @@ def test_conftest_non_python_items(recwarn, testdir): checkfile = testdir.makefile(ext="xxx", hello="world") testdir.makepyfile(x="") testdir.maketxtfile(x="") - config = testdir.parseconfig() recwarn.clear() - dircol = config.getnode(checkfile.dirpath()) + config = testdir.parseconfig() + dircol = testdir.getnode(config, checkfile.dirpath()) + w = recwarn.pop(DeprecationWarning) assert str(w.message).find("conftest.py") != -1 colitems = dircol.collect() @@ -288,7 +292,7 @@ def test_conftest_non_python_items(recwarn, testdir): assert colitems[0].name == "hello.xxx" assert colitems[0].__class__.__name__ == "CustomItem" - item = config.getnode(checkfile) + item = testdir.getnode(config, checkfile) assert item.name == "hello.xxx" assert item.__class__.__name__ == "CustomItem" @@ -321,14 +325,14 @@ def test_extra_python_files_and_functions(testdir, recwarn): """) # check that directory collects "check_" files config = testdir.parseconfig() - col = config.getnode(checkfile.dirpath()) + col = testdir.getnode(config, checkfile.dirpath()) colitems = col.collect() assert len(colitems) == 1 assert isinstance(colitems[0], py.test.collect.Module) # check that module collects "check_" functions and methods config = testdir.parseconfig(checkfile) - col = config.getnode(checkfile) + col = testdir.getnode(config, checkfile) assert isinstance(col, py.test.collect.Module) colitems = col.collect() assert len(colitems) == 2 diff --git a/testing/test_pycollect.py b/testing/test_pycollect.py index feb3f418f..bc5a8b068 100644 --- a/testing/test_pycollect.py +++ b/testing/test_pycollect.py @@ -247,10 +247,11 @@ class TestFunction: param = 1 funcargs = {} id = "world" + collection = object() f5 = py.test.collect.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance) + callspec=callspec1, callobj=isinstance, collection=collection) f5b = py.test.collect.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance) + callspec=callspec2, callobj=isinstance, collection=collection) assert f5 != f5b assert not (f5 == f5b) @@ -459,8 +460,8 @@ def test_generate_tests_only_done_in_subdir(testdir): def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) - trail = modcol.config._rootcol.totrail(modcol) - newcol = modcol.config._rootcol.fromtrail(trail) + trail = modcol.collection.getid(modcol) + newcol = modcol.collection.getbyid(trail)[0] assert modcol.name == newcol.name diff --git a/testing/test_session.py b/testing/test_session.py index a09d3bc30..5bdc51a3a 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,11 +1,6 @@ import py class SessionTests: - def test_initsession(self, testdir, tmpdir): - config = testdir.reparseconfig() - session = config.initsession() - assert session.config is config - def test_basic_testitem_events(self, testdir): tfile = testdir.makepyfile(""" def test_one(): @@ -25,11 +20,11 @@ class SessionTests: assert failed[0].item.name == "test_one_one" assert failed[1].item.name == "test_other" assert failed[2].item.name == "test_two" - itemstarted = reprec.getcalls("pytest_itemstart") + itemstarted = reprec.getcalls("pytest_log_itemcollect") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 - col = colstarted[0].collector + assert len(colstarted) == 1 + 1 # XXX ExtraTopCollector + col = colstarted[1].collector assert isinstance(col, py.test.collect.Module) def test_nested_import_error(self, testdir): @@ -183,13 +178,13 @@ class TestNewSession(SessionTests): ) reprec = testdir.inline_run('--collectonly', p.dirpath()) - itemstarted = reprec.getcalls("pytest_itemstart") + itemstarted = reprec.getcalls("pytest_log_itemcollect") assert len(itemstarted) == 3 assert not reprec.getreports("pytest_runtest_logreport") started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 8 + assert len(started) == 8 + 1 # XXX extra TopCollector colfail = [x for x in finished if x.failed] colskipped = [x for x in finished if x.skipped] assert len(colfail) == 1 From 58169edc8e13b5d94fb7c51ddd26497ab55c9a3b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 16 Sep 2010 01:06:07 +0100 Subject: [PATCH 18/38] Add set comparison Also add a (too) simple mechanism too truncate too long explanations. --HG-- branch : trunk --- py/_plugin/pytest_assertion.py | 55 +++++++++++++++++++------ testing/plugin/test_pytest_assertion.py | 4 ++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 1e5452f07..8f2aa5b22 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -30,25 +30,39 @@ def warn_about_missing_assertion(): def pytest_assert_compare(op, left, right): """Make a specialised explanation for comapare equal""" - if op != '==' or type(left) != type(right): + if type(left) != type(right): return None - explanation = [] + left_repr = py.io.saferepr(left, maxsize=30) right_repr = py.io.saferepr(right, maxsize=30) - explanation += ['%s == %s' % (left_repr, right_repr)] + summary = '%s %s %s' % (left_repr, op, right_repr) + issquence = lambda x: isinstance(x, (list, tuple)) istext = lambda x: isinstance(x, basestring) isdict = lambda x: isinstance(x, dict) - if istext(left): - explanation += [line.strip('\n') for line in - py.std.difflib.ndiff(left.splitlines(), right.splitlines())] - elif issquence(left): - explanation += _compare_eq_sequence(left, right) - elif isdict(left): - explanation += _pprint_diff(left, right) - else: - return None # No specialised knowledge - return explanation + isset = lambda: isinstance(left, set) + + explanation = None + if op == '==': + if istext(left): + explanation = [line.strip('\n') for line in + py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + elif issquence(left): + explanation = _compare_eq_sequence(left, right) + elif isset(): + explanation = _compare_eq_set(left, right) + elif isdict(left): + explanation = _pprint_diff(left, right) + + if not explanation: + return None + + # Don't include pageloads of data, should be configurable + if len(''.join(explanation)) > 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation def _compare_eq_sequence(left, right): @@ -72,3 +86,18 @@ def _pprint_diff(left, right): return [line.strip('\n') for line in py.std.difflib.ndiff(py.std.pprint.pformat(left).splitlines(), py.std.pprint.pformat(right).splitlines())] + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('Extra items in the left set:') + for item in diff_left: + explanation.append(py.io.saferepr(item)) + if diff_right: + explanation.append('Extra items in the right set:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 4c34a0797..9ab007f29 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -81,3 +81,7 @@ class Test_pytest_assert_compare: def test_dict(self): expl = plugin.pytest_assert_compare('==', {'a': 0}, {'a': 1}) assert len(expl) > 1 + + def test_set(self): + expl = plugin.pytest_assert_compare('==', set([0, 1]), set([0, 2])) + assert len(expl) > 1 From 0af90e0962fde4532f773ac661cb63c051c38c37 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 16 Sep 2010 01:07:53 +0100 Subject: [PATCH 19/38] Add specialised explanations to the demo This currently breaks the test_failuers.py example as that file counts the number of failures in the demo. But this demo isn't fixed yet so we'll leave it for now. --HG-- branch : trunk --- doc/example/assertion/failure_demo.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/example/assertion/failure_demo.py b/doc/example/assertion/failure_demo.py index 5fba99db7..b99253ee8 100644 --- a/doc/example/assertion/failure_demo.py +++ b/doc/example/assertion/failure_demo.py @@ -118,5 +118,25 @@ def test_dynamic_compile_shows_nicely(): module.foo() +class TestSpecialisedExplanations(object): + def test_eq_text(self): + assert 'spam' == 'eggs' + + def test_eq_similar_text(self): + assert 'foo 1 bar' == 'foo 2 bar' + + def test_eq_multiline_text(self): + assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + + def test_eq_list(self): + assert [0, 1, 2] == [0, 1, 3] + + def test_eq_dict(self): + assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} + + def test_eq_set(self): + assert set([0, 10, 11, 12]) == set([0, 20, 21]) + + def globf(x): return x+1 From abab8f6f63783cbd22f30744d805e449881469d3 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sat, 18 Sep 2010 13:03:28 +0100 Subject: [PATCH 20/38] Move all tests to test_pytest_assertion The py.code code is independent of any py.test specifics so we should avoid creating dependencies on py.test in those parts. --HG-- branch : trunk --- testing/code/test_assertionnew.py | 44 ------------------------ testing/plugin/test_pytest_assertion.py | 45 ++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 45 deletions(-) delete mode 100644 testing/code/test_assertionnew.py diff --git a/testing/code/test_assertionnew.py b/testing/code/test_assertionnew.py deleted file mode 100644 index 8511f8125..000000000 --- a/testing/code/test_assertionnew.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys - -import py -from py._code._assertionnew import interpret - - -def getframe(): - """Return the frame of the caller as a py.code.Frame object""" - return py.code.Frame(sys._getframe(1)) - - -def pytest_funcarg__hook(request): - class MockHook(object): - def __init__(self): - self.called = False - self.args = tuple() - self.kwargs = dict() - - def __call__(self, op, left, right): - self.called = True - self.op = op - self.left = left - self.right = right - return MockHook() - - -def test_pytest_assert_compare_called(monkeypatch, hook): - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_compare', hook) - interpret('assert 0 == 1', getframe()) - assert hook.called - - -def test_pytest_assert_compare_args(monkeypatch, hook): - print hook.called - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_compare', hook) - interpret('assert [0, 1] == [0, 2]', getframe()) - print hook.called - print hook.left - print hook.right - assert hook.op == '==' - assert hook.left == [0, 1] - assert hook.right == [0, 2] diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 9ab007f29..596de0050 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -1,7 +1,30 @@ +import sys + import py +from py._code._assertionnew import interpret import py._plugin.pytest_assertion as plugin +def getframe(): + """Return the frame of the caller as a py.code.Frame object""" + return py.code.Frame(sys._getframe(1)) + + +def pytest_funcarg__hook(request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() + + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + return MockHook() + + def test_functional(testdir): testdir.makepyfile(""" def test_hello(): @@ -54,7 +77,27 @@ def test_traceback_failure(testdir): ]) -class Test_pytest_assert_compare: +def test_pytest_assert_compare_called(monkeypatch, hook): + monkeypatch.setattr(py._plugin.pytest_assertion, + 'pytest_assert_compare', hook) + interpret('assert 0 == 1', getframe()) + assert hook.called + + +def test_pytest_assert_compare_args(monkeypatch, hook): + print hook.called + monkeypatch.setattr(py._plugin.pytest_assertion, + 'pytest_assert_compare', hook) + interpret('assert [0, 1] == [0, 2]', getframe()) + print hook.called + print hook.left + print hook.right + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] + + +class TestAssertCompare: def test_different_types(self): assert plugin.pytest_assert_compare('==', [0, 1], 'foo') is None From b86207a6c1f0ad58a265cc206cff41e07809058f Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Sep 2010 00:26:12 +0100 Subject: [PATCH 21/38] Don't load py.test.config inside py._code._assertionnew Loading py.test.config triggers py.test initialisation while py.code should stay independent of py.test. By adding the hook as an attribute to py.test AssertionError py.code can get access to the hooks only when py.test is loaded already. --HG-- branch : trunk --- py/_code/_assertionnew.py | 23 +++++++++++++++-------- py/_plugin/pytest_assertion.py | 5 +++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index a80ae898c..192eecfce 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -108,10 +108,16 @@ unary_map = { class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information.""" + """Interpret AST nodes to gleam useful debugging information. + + The _pytesthook attribute is used to detect if the py.test + pytest_assertion plugin is loaded and if so call it's hooks. + """ def __init__(self, frame): self.frame = frame + self._pytesthook = getattr(py.builtin.builtins.AssertionError, + "_pytesthook") def generic_visit(self, node): # Fallback when we don't have a special implementation. @@ -177,13 +183,14 @@ class DebugInterpreter(ast.NodeVisitor): if not result: break left_explanation, left_result = next_explanation, next_result - hook_result = py.test.config.hook.pytest_assert_compare( - op=op_symbol, left=left_result, right=next_result) - if hook_result: - for new_expl in hook_result: - if new_expl: - explanation = '\n~'.join(new_expl) - break + if self._pytesthook: + hook_result = self._pytesthook.pytest_assert_compare( + op=op_symbol, left=left_result, right=next_result) + if hook_result: + for new_expl in hook_result: + if new_expl: + explanation = '\n~'.join(new_expl) + break return explanation, result def visit_BoolOp(self, boolop): diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 8f2aa5b22..2816671ab 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -8,10 +8,15 @@ def pytest_addoption(parser): help="disable python assert expression reinterpretation."), def pytest_configure(config): + # The _pytesthook attribute on the AssertionError is used by + # py._code._assertionnew to detect this plugin was loaded and in + # turn call the hooks defined here as part of the + # DebugInterpreter. if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() config._oldassertion = py.builtin.builtins.AssertionError py.builtin.builtins.AssertionError = py.code._AssertionError + py.builtin.builtins.AssertionError._pytesthook = config.hook def pytest_unconfigure(config): if hasattr(config, '_oldassertion'): From ca84a5e8e0ca66600c13fbacf0358088f2e1a929 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Sep 2010 00:56:39 +0100 Subject: [PATCH 22/38] Rename pytest_assert_compare to pytest_assert_binrepr Holger prefers to only have one hook and it also turns out that "in" is actually a ast.Compare node as well too. This also modifies the pytest_assert_binrepr hook slightly so that it's more accomodating to other operators then just compare (i.e. don't bail out as soon as the types of the operands differ). --HG-- branch : trunk --- py/_code/_assertionnew.py | 2 +- py/_plugin/hookspec.py | 11 ++++++----- py/_plugin/pytest_assertion.py | 22 +++++++++++----------- testing/plugin/test_pytest_assertion.py | 22 +++++++++++----------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 192eecfce..2c40a93dc 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -184,7 +184,7 @@ class DebugInterpreter(ast.NodeVisitor): break left_explanation, left_result = next_explanation, next_result if self._pytesthook: - hook_result = self._pytesthook.pytest_assert_compare( + hook_result = self._pytesthook.pytest_assert_binrepr( op=op_symbol, left=left_result, right=next_result) if hook_result: for new_expl in hook_result: diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index f295211cd..9f34c4598 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -127,13 +127,14 @@ def pytest_sessionfinish(session, exitstatus): # hooks for customising the assert methods # ------------------------------------------------------------------------- -def pytest_assert_compare(op, left, right): - """Customise compare assertion +def pytest_assert_binrepr(op, left, right): + """Customise explanation for binary operators - Return None or an empty list for no custom compare, otherwise + Return None or an empty list for no custom explanation, otherwise return a list of strings. The strings will be joined by newlines - but any newlines *in* as string will be escaped. Note that all - but the first line will be indented sligthly. + but any newlines *in* a string will be escaped. Note that all but + the first line will be indented sligthly, the intention is for the + first line to be a summary. """ # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 2816671ab..347f7edb7 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -33,32 +33,32 @@ def warn_about_missing_assertion(): " (are you using python -O?)") -def pytest_assert_compare(op, left, right): - """Make a specialised explanation for comapare equal""" - if type(left) != type(right): - return None - +def pytest_assert_binrepr(op, left, right): + """Make specialised explanations for some operators/operands""" left_repr = py.io.saferepr(left, maxsize=30) right_repr = py.io.saferepr(right, maxsize=30) summary = '%s %s %s' % (left_repr, op, right_repr) - issquence = lambda x: isinstance(x, (list, tuple)) + issequence = lambda x: isinstance(x, (list, tuple)) istext = lambda x: isinstance(x, basestring) isdict = lambda x: isinstance(x, dict) - isset = lambda: isinstance(left, set) + isset = lambda x: isinstance(x, set) explanation = None if op == '==': - if istext(left): + if istext(left) and istext(right): explanation = [line.strip('\n') for line in py.std.difflib.ndiff(left.splitlines(), right.splitlines())] - elif issquence(left): + elif issequence(left) and issequence(right): explanation = _compare_eq_sequence(left, right) - elif isset(): + elif isset(left) and isset(right): explanation = _compare_eq_set(left, right) - elif isdict(left): + elif isdict(left) and isdict(right): explanation = _pprint_diff(left, right) + elif op == 'in': + # XXX + pass if not explanation: return None diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 596de0050..81796dd12 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -77,17 +77,17 @@ def test_traceback_failure(testdir): ]) -def test_pytest_assert_compare_called(monkeypatch, hook): +def test_pytest_assert_binrepr_called(monkeypatch, hook): monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_compare', hook) + 'pytest_assert_binrepr', hook) interpret('assert 0 == 1', getframe()) assert hook.called -def test_pytest_assert_compare_args(monkeypatch, hook): +def test_pytest_assert_binrepr_args(monkeypatch, hook): print hook.called monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_compare', hook) + 'pytest_assert_binrepr', hook) interpret('assert [0, 1] == [0, 2]', getframe()) print hook.called print hook.left @@ -99,32 +99,32 @@ def test_pytest_assert_compare_args(monkeypatch, hook): class TestAssertCompare: def test_different_types(self): - assert plugin.pytest_assert_compare('==', [0, 1], 'foo') is None + assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None def test_summary(self): - summary = plugin.pytest_assert_compare('==', [0, 1], [0, 2])[0] + summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] assert len(summary) < 65 def test_text_diff(self): - diff = plugin.pytest_assert_compare('==', 'spam', 'eggs')[1:] + diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] assert '- spam' in diff assert '+ eggs' in diff def test_multiline_text_diff(self): left = 'foo\nspam\nbar' right = 'foo\neggs\nbar' - diff = plugin.pytest_assert_compare('==', left, right) + diff = plugin.pytest_assert_binrepr('==', left, right) assert '- spam' in diff assert '+ eggs' in diff def test_list(self): - expl = plugin.pytest_assert_compare('==', [0, 1], [0, 2]) + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) assert len(expl) > 1 def test_dict(self): - expl = plugin.pytest_assert_compare('==', {'a': 0}, {'a': 1}) + expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) assert len(expl) > 1 def test_set(self): - expl = plugin.pytest_assert_compare('==', set([0, 1]), set([0, 2])) + expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) assert len(expl) > 1 From 56b955dfb572e2e76006bb69893c58e8b9d67028 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Sep 2010 18:42:04 +0100 Subject: [PATCH 23/38] Make pytest_assert_binrepr work on python3 too --HG-- branch : trunk --- py/_plugin/pytest_assertion.py | 9 ++++++++- testing/plugin/test_pytest_assertion.py | 4 ---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 347f7edb7..226da79c7 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -33,6 +33,13 @@ def warn_about_missing_assertion(): " (are you using python -O?)") +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + def pytest_assert_binrepr(op, left, right): """Make specialised explanations for some operators/operands""" left_repr = py.io.saferepr(left, maxsize=30) @@ -72,7 +79,7 @@ def pytest_assert_binrepr(op, left, right): def _compare_eq_sequence(left, right): explanation = [] - for i in xrange(min(len(left), len(right))): + for i in range(min(len(left), len(right))): if left[i] != right[i]: explanation += ['First differing item %s: %s != %s' % (i, left[i], right[i])] diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 81796dd12..9d750896f 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -85,13 +85,9 @@ def test_pytest_assert_binrepr_called(monkeypatch, hook): def test_pytest_assert_binrepr_args(monkeypatch, hook): - print hook.called monkeypatch.setattr(py._plugin.pytest_assertion, 'pytest_assert_binrepr', hook) interpret('assert [0, 1] == [0, 2]', getframe()) - print hook.called - print hook.left - print hook.right assert hook.op == '==' assert hook.left == [0, 1] assert hook.right == [0, 2] From c3166ee84a5b0529c5e1a4f4db391b0b82a65fd0 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Sep 2010 18:52:07 +0100 Subject: [PATCH 24/38] Fix bug when the right list was longer then the left Thanks to Holger for finding this. --HG-- branch : trunk --- py/_plugin/pytest_assertion.py | 2 +- testing/plugin/test_pytest_assertion.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 226da79c7..9e6355f41 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -89,7 +89,7 @@ def _compare_eq_sequence(left, right): 'first extra item: %s' % left[len(right)]] elif len(left) < len(right): explanation += ['Right contains more items, ' - 'first extra item: %s' % right[len(right)]] + 'first extra item: %s' % right[len(left)]] return explanation + _pprint_diff(left, right) diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 9d750896f..abada10b2 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -117,6 +117,12 @@ class TestAssertCompare: expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) assert len(expl) > 1 + def test_list_different_lenghts(self): + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) + assert len(expl) > 1 + expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) + assert len(expl) > 1 + def test_dict(self): expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) assert len(expl) > 1 From 2cf22e312412748bb75f0979f07331ce588d36e6 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 25 Sep 2010 18:23:26 +0200 Subject: [PATCH 25/38] shift all python related testing functioanlity to a dedicated pytest_python plugin which incorporates pytest's logic of python function testing (including funcargs). --HG-- branch : trunk --- CHANGELOG | 1 + py/__init__.py | 6 - py/_plugin/pytest_default.py | 72 +-- .../pycollect.py => _plugin/pytest_python.py} | 316 ++++++++++- py/_plugin/pytest_terminal.py | 3 - py/_test/funcargs.py | 228 -------- py/_test/pluginmanager.py | 2 +- .../test_pytest_python.py} | 518 +++++++++++++++++- testing/plugin/test_pytest_tmpdir.py | 2 +- testing/test_pycollect.py | 517 ----------------- 10 files changed, 830 insertions(+), 835 deletions(-) rename py/{_test/pycollect.py => _plugin/pytest_python.py} (55%) delete mode 100644 py/_test/funcargs.py rename testing/{test_funcargs.py => plugin/test_pytest_python.py} (53%) delete mode 100644 testing/test_pycollect.py diff --git a/CHANGELOG b/CHANGELOG index d9cf05000..a865f0cd6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Changes between 1.3.4 and 1.4.0a1 ================================================== - major refactoring of internal collection handling +- majorly reduce py.test core code, shift function/python testing to own plugin - fix issue88 (finding custom test nodes from command line arg) Changes between 1.3.3 and 1.3.4 diff --git a/py/__init__.py b/py/__init__.py index 0a831a41c..e62e1a72a 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -45,12 +45,6 @@ py.apipkg.initpkg(__name__, dict( 'Directory' : '._test.collect:Directory', 'File' : '._test.collect:File', 'Item' : '._test.collect:Item', - 'Module' : '._test.pycollect:Module', - 'Class' : '._test.pycollect:Class', - 'Instance' : '._test.pycollect:Instance', - 'Generator' : '._test.pycollect:Generator', - 'Function' : '._test.pycollect:Function', - '_fillfuncargs' : '._test.funcargs:fillfuncargs', }, 'cmdline': { 'main' : '._test.session:main', # backward compat diff --git a/py/_plugin/pytest_default.py b/py/_plugin/pytest_default.py index 41f320754..45f13376a 100644 --- a/py/_plugin/pytest_default.py +++ b/py/_plugin/pytest_default.py @@ -5,45 +5,15 @@ import py def pytest_cmdline_main(config): from py._test.session import Session, Collection - exitstatus = 0 - if config.option.showfuncargs: - from py._test.funcargs import showfuncargs - session = showfuncargs(config) - else: - collection = Collection(config) - # instantiate session already because it - # records failures and implements maxfail handling - session = Session(config, collection) - exitstatus = collection.do_collection() - if not exitstatus: - exitstatus = session.main() + collection = Collection(config) + # instantiate session already because it + # records failures and implements maxfail handling + session = Session(config, collection) + exitstatus = collection.do_collection() + if not exitstatus: + exitstatus = session.main() return exitstatus -def pytest_pyfunc_call(__multicall__, pyfuncitem): - if not __multicall__.execute(): - testfunction = pyfuncitem.obj - if pyfuncitem._isyieldedfunction(): - testfunction(*pyfuncitem._args) - else: - funcargs = pyfuncitem.funcargs - testfunction(**funcargs) - -def pytest_collect_file(path, parent): - ext = path.ext - pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.collection._argfspaths: - if ext == ".py": - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) - -def pytest_pycollect_makemodule(path, parent): - return parent.Module(path, parent) - -def pytest_funcarg__pytestconfig(request): - """ the pytest config object with access to command line opts.""" - return request.config - def pytest_ignore_collect(path, config): ignore_paths = config.getconftest_pathlist("collect_ignore", path=path) ignore_paths = ignore_paths or [] @@ -57,7 +27,6 @@ def pytest_ignore_collect(path, config): if path == p or path.relto(p): return True - def pytest_collect_directory(path, parent): # XXX reconsider the following comment # not use parent.Directory here as we generally @@ -105,30 +74,3 @@ def pytest_configure(config): if config.getvalue("exitfirst"): config.option.maxfail = 1 -# pycollect related hooks and code, should move to pytest_pycollect.py - -def pytest_pycollect_makeitem(__multicall__, collector, name, obj): - res = __multicall__.execute() - if res is not None: - return res - if collector._istestclasscandidate(name, obj): - res = collector._deprecated_join(name) - if res is not None: - return res - return collector.Class(name, parent=collector) - elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): - res = collector._deprecated_join(name) - if res is not None: - return res - if is_generator(obj): - # XXX deprecation warning - return collector.Generator(name, parent=collector) - else: - return collector._genfunctions(name, obj) - -def is_generator(func): - try: - return py.code.getrawcode(func).co_flags & 32 # generator function - except AttributeError: # builtin functions have no bytecode - # assume them to not be generators - return False diff --git a/py/_test/pycollect.py b/py/_plugin/pytest_python.py similarity index 55% rename from py/_test/pycollect.py rename to py/_plugin/pytest_python.py index e4514fc31..337d4919f 100644 --- a/py/_test/pycollect.py +++ b/py/_plugin/pytest_python.py @@ -5,9 +5,80 @@ import py import inspect import sys from py._test.collect import configproperty, warnoldcollect -from py._test import funcargs from py._code.code import TerminalRepr +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group._addoption('--funcargs', + action="store_true", dest="showfuncargs", default=False, + help="show available function arguments, sorted by plugin") + +def pytest_cmdline_main(config): + if config.option.showfuncargs: + showfuncargs(config) + return 0 + +def pytest_namespace(): + # XXX rather return than set directly + py.test.collect.Module = Module + py.test.collect.Class = Class + py.test.collect.Instance = Instance + py.test.collect.Function = Function + py.test.collect.Generator = Generator + py.test.collect._fillfuncargs = fillfuncargs + +def pytest_funcarg__pytestconfig(request): + """ the pytest config object with access to command line opts.""" + return request.config + +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) + else: + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + +def pytest_collect_file(path, parent): + ext = path.ext + pb = path.purebasename + if pb.startswith("test_") or pb.endswith("_test") or \ + path in parent.collection._argfspaths: + if ext == ".py": + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) + +def pytest_pycollect_makemodule(path, parent): + return parent.Module(path, parent) + + +def pytest_pycollect_makeitem(__multicall__, collector, name, obj): + res = __multicall__.execute() + if res is not None: + return res + if collector._istestclasscandidate(name, obj): + res = collector._deprecated_join(name) + if res is not None: + return res + return collector.Class(name, parent=collector) + elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): + res = collector._deprecated_join(name) + if res is not None: + return res + if is_generator(obj): + # XXX deprecation warning + return collector.Generator(name, parent=collector) + else: + return collector._genfunctions(name, obj) + +def is_generator(func): + try: + return py.code.getrawcode(func).co_flags & 32 # generator function + except AttributeError: # builtin functions have no bytecode + # assume them to not be generators + return False + class PyobjMixin(object): def obj(): def fget(self): @@ -120,10 +191,10 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): module = self.getparent(Module).obj clscol = self.getparent(Class) cls = clscol and clscol.obj or None - metafunc = funcargs.Metafunc(funcobj, config=self.config, + metafunc = Metafunc(funcobj, config=self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests - plugins = funcargs.getplugins(self, withpy=True) + plugins = getplugins(self, withpy=True) gentesthook.pcall(plugins, metafunc=metafunc) if not metafunc._calls: return self.Function(name, parent=self) @@ -212,16 +283,9 @@ class Class(PyCollectorMixin, py.test.collect.Collector): class Instance(PyCollectorMixin, py.test.collect.Collector): def _getobj(self): return self.parent.obj() - def Function(self): - return getattr(self.obj, 'Function', - PyCollectorMixin.Function.__get__(self)) # XXX for python 2.2 + def _keywords(self): return [] - Function = property(Function) - - #def __repr__(self): - # return "<%s of '%s'>" %(self.__class__.__name__, - # self.parent.obj.__name__) def newinstance(self): self.obj = self._getobj() @@ -270,7 +334,7 @@ class FunctionMixin(PyobjMixin): return traceback def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(funcargs.FuncargRequest.LookupError): + if excinfo.errisinstance(FuncargRequest.LookupError): fspath, lineno, msg = self.reportinfo() lines, _ = inspect.getsourcelines(self.obj) for i, line in enumerate(lines): @@ -384,7 +448,7 @@ class Function(FunctionMixin, py.test.collect.Item): def setup(self): super(Function, self).setup() if hasattr(self, 'funcargs'): - funcargs.fillfuncargs(self) + fillfuncargs(self) def __eq__(self, other): try: @@ -410,3 +474,229 @@ def hasinit(obj): if init: if init != object.__init__: return True + + +def getfuncargnames(function): + 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', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] + +def fillfuncargs(function): + """ fill missing funcargs. """ + request = FuncargRequest(pyfuncitem=function) + request._fillfuncargs() + +def getplugins(node, withpy=False): # might by any node + plugins = node.config._getmatchingplugins(node.fspath) + if withpy: + mod = node.getparent(py.test.collect.Module) + if mod is not None: + plugins.append(mod.obj) + inst = node.getparent(py.test.collect.Instance) + if inst is not None: + plugins.append(inst.obj) + return plugins + +_notexists = object() +class CallSpec: + def __init__(self, funcargs, id, param): + self.funcargs = funcargs + self.id = id + if param is not _notexists: + self.param = param + def __repr__(self): + return "" %( + self.id, getattr(self, 'param', '?'), self.funcargs) + +class Metafunc: + def __init__(self, function, config=None, cls=None, module=None): + self.config = config + self.module = module + self.function = function + self.funcargnames = getfuncargnames(function) + self.cls = cls + self.module = module + self._calls = [] + self._ids = py.builtin.set() + + def addcall(self, funcargs=None, id=_notexists, param=_notexists): + assert funcargs is None or isinstance(funcargs, dict) + if id is None: + raise ValueError("id=None not allowed") + if id is _notexists: + id = len(self._calls) + id = str(id) + if id in self._ids: + raise ValueError("duplicate id %r" % id) + self._ids.add(id) + self._calls.append(CallSpec(funcargs, id, param)) + +class FuncargRequest: + _argprefix = "pytest_funcarg__" + _argname = None + + class LookupError(LookupError): + """ error on performing funcarg request. """ + + def __init__(self, pyfuncitem): + self._pyfuncitem = pyfuncitem + self.function = pyfuncitem.obj + self.module = pyfuncitem.getparent(py.test.collect.Module).obj + clscol = pyfuncitem.getparent(py.test.collect.Class) + self.cls = clscol and clscol.obj or None + self.instance = py.builtin._getimself(self.function) + self.config = pyfuncitem.config + self.fspath = pyfuncitem.fspath + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam + self._plugins = getplugins(pyfuncitem, withpy=True) + self._funcargs = self._pyfuncitem.funcargs.copy() + self._name2factory = {} + self._currentarg = None + + def _fillfuncargs(self): + argnames = getfuncargnames(self.function) + if argnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) + + + def applymarker(self, marker): + """ apply a marker to a test function invocation. + + The 'marker' must be created with py.test.mark.* XYZ. + """ + if not isinstance(marker, py.test.mark.XYZ.__class__): + raise ValueError("%r is not a py.test.mark.* object") + self._pyfuncitem.keywords[marker.markname] = marker + + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + """ cache and return result of calling setup(). + + The requested argument name, the scope and the ``extrakey`` + determine the cache key. The scope also determines when + teardown(result) will be called. valid scopes are: + scope == 'function': when the single test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'session': when tests of the session have run. + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self._addfinalizer(finalizer, scope=scope) + return val + + def getfuncargvalue(self, argname): + try: + return self._funcargs[argname] + except KeyError: + pass + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + self._currentarg = argname + try: + self._funcargs[argname] = res = funcargfactory(request=self) + finally: + self._currentarg = oldarg + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem.getparent(py.test.collect.Module) + elif scope == "session": + return None + raise ValueError("unknown finalization scope %r" %(scope,)) + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) + + def addfinalizer(self, finalizer): + """ call the given finalizer after test function finished execution. """ + self._addfinalizer(finalizer, scope="function") + + def __repr__(self): + return "" %(self._pyfuncitem) + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + +def showfuncargs(config): + from py._test.session import Collection + collection = Collection(config) + colitem = collection.getinitialnodes()[0] + curdir = py.path.local() + tw = py.io.TerminalWriter() + plugins = getplugins(colitem, withpy=True) + verbose = config.getvalue("verbose") + for plugin in plugins: + available = [] + for name, factory in vars(plugin).items(): + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] + if name not in available: + available.append([name, factory]) + if available: + pluginname = plugin.__name__ + for name, factory in available: + loc = getlocation(factory, curdir) + if verbose: + funcargspec = "%s -- %s" %(name, loc,) + else: + funcargspec = name + tw.line(funcargspec, green=True) + doc = factory.__doc__ or "" + if doc: + for line in doc.split("\n"): + tw.line(" " + line.strip()) + else: + tw.line(" %s: no docstring available" %(loc,), + red=True) + +def getlocation(function, curdir): + import inspect + fn = py.path.local(inspect.getfile(function)) + lineno = py.builtin._getcode(function).co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" %(fn, lineno+1) diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index 7fac140bb..bed5630e3 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -27,9 +27,6 @@ def pytest_addoption(parser): group._addoption('--fulltrace', action="store_true", dest="fulltrace", default=False, help="don't cut any tracebacks (default is to cut).") - group._addoption('--funcargs', - action="store_true", dest="showfuncargs", default=False, - help="show available function arguments, sorted by plugin") def pytest_configure(config): if config.option.collectonly: diff --git a/py/_test/funcargs.py b/py/_test/funcargs.py deleted file mode 100644 index 2b4f9a0ca..000000000 --- a/py/_test/funcargs.py +++ /dev/null @@ -1,228 +0,0 @@ -import py - -def getfuncargnames(function): - 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', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return argnames[startindex:-numdefaults] - return argnames[startindex:] - -def fillfuncargs(function): - """ fill missing funcargs. """ - request = FuncargRequest(pyfuncitem=function) - request._fillfuncargs() - -def getplugins(node, withpy=False): # might by any node - plugins = node.config._getmatchingplugins(node.fspath) - if withpy: - mod = node.getparent(py.test.collect.Module) - if mod is not None: - plugins.append(mod.obj) - inst = node.getparent(py.test.collect.Instance) - if inst is not None: - plugins.append(inst.obj) - return plugins - -_notexists = object() -class CallSpec: - def __init__(self, funcargs, id, param): - self.funcargs = funcargs - self.id = id - if param is not _notexists: - self.param = param - def __repr__(self): - return "" %( - self.id, getattr(self, 'param', '?'), self.funcargs) - -class Metafunc: - def __init__(self, function, config=None, cls=None, module=None): - self.config = config - self.module = module - self.function = function - self.funcargnames = getfuncargnames(function) - self.cls = cls - self.module = module - self._calls = [] - self._ids = py.builtin.set() - - def addcall(self, funcargs=None, id=_notexists, param=_notexists): - assert funcargs is None or isinstance(funcargs, dict) - if id is None: - raise ValueError("id=None not allowed") - if id is _notexists: - id = len(self._calls) - id = str(id) - if id in self._ids: - raise ValueError("duplicate id %r" % id) - self._ids.add(id) - self._calls.append(CallSpec(funcargs, id, param)) - -class FuncargRequest: - _argprefix = "pytest_funcarg__" - _argname = None - - class LookupError(LookupError): - """ error on performing funcarg request. """ - - def __init__(self, pyfuncitem): - self._pyfuncitem = pyfuncitem - self.function = pyfuncitem.obj - self.module = pyfuncitem.getparent(py.test.collect.Module).obj - clscol = pyfuncitem.getparent(py.test.collect.Class) - self.cls = clscol and clscol.obj or None - self.instance = py.builtin._getimself(self.function) - self.config = pyfuncitem.config - self.fspath = pyfuncitem.fspath - if hasattr(pyfuncitem, '_requestparam'): - self.param = pyfuncitem._requestparam - self._plugins = getplugins(pyfuncitem, withpy=True) - self._funcargs = self._pyfuncitem.funcargs.copy() - self._name2factory = {} - self._currentarg = None - - def _fillfuncargs(self): - argnames = getfuncargnames(self.function) - if argnames: - assert not getattr(self._pyfuncitem, '_args', None), ( - "yielded functions cannot have funcargs") - for argname in argnames: - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) - - - def applymarker(self, marker): - """ apply a marker to a test function invocation. - - The 'marker' must be created with py.test.mark.* XYZ. - """ - if not isinstance(marker, py.test.mark.XYZ.__class__): - raise ValueError("%r is not a py.test.mark.* object") - self._pyfuncitem.keywords[marker.markname] = marker - - def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): - """ cache and return result of calling setup(). - - The requested argument name, the scope and the ``extrakey`` - determine the cache key. The scope also determines when - teardown(result) will be called. valid scopes are: - scope == 'function': when the single test function run finishes. - scope == 'module': when tests in a different module are run - scope == 'session': when tests of the session have run. - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self._addfinalizer(finalizer, scope=scope) - return val - - def getfuncargvalue(self, argname): - try: - return self._funcargs[argname] - except KeyError: - pass - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - self._currentarg = argname - try: - self._funcargs[argname] = res = funcargfactory(request=self) - finally: - self._currentarg = oldarg - return res - - def _getscopeitem(self, scope): - if scope == "function": - return self._pyfuncitem - elif scope == "module": - return self._pyfuncitem.getparent(py.test.collect.Module) - elif scope == "session": - return None - raise ValueError("unknown finalization scope %r" %(scope,)) - - def _addfinalizer(self, finalizer, scope): - colitem = self._getscopeitem(scope) - self.config._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem) - - def addfinalizer(self, finalizer): - """ call the given finalizer after test function finished execution. """ - self._addfinalizer(finalizer, scope="function") - - def __repr__(self): - return "" %(self._pyfuncitem) - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self._pyfuncitem.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) - -def showfuncargs(config): - from py._test.session import Collection - collection = Collection(config) - colitem = collection.getinitialnodes()[0] - curdir = py.path.local() - tw = py.io.TerminalWriter() - #from py._test.funcargs import getplugins - #from py._test.funcargs import FuncargRequest - plugins = getplugins(colitem, withpy=True) - verbose = config.getvalue("verbose") - for plugin in plugins: - available = [] - for name, factory in vars(plugin).items(): - if name.startswith(FuncargRequest._argprefix): - name = name[len(FuncargRequest._argprefix):] - if name not in available: - available.append([name, factory]) - if available: - pluginname = plugin.__name__ - for name, factory in available: - loc = getlocation(factory, curdir) - if verbose: - funcargspec = "%s -- %s" %(name, loc,) - else: - funcargspec = name - tw.line(funcargspec, green=True) - doc = factory.__doc__ or "" - if doc: - for line in doc.split("\n"): - tw.line(" " + line.strip()) - else: - tw.line(" %s: no docstring available" %(loc,), - red=True) - -def getlocation(function, curdir): - import inspect - fn = py.path.local(inspect.getfile(function)) - lineno = py.builtin._getcode(function).co_firstlineno - if fn.relto(curdir): - fn = fn.relto(curdir) - return "%s:%d" %(fn, lineno+1) diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index b4ed48528..32abedf65 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -6,7 +6,7 @@ import inspect from py._plugin import hookspec default_plugins = ( - "default runner pdb capture mark terminal skipping tmpdir monkeypatch " + "default python runner pdb capture mark terminal skipping tmpdir monkeypatch " "recwarn pastebin unittest helpconfig nose assertion genscript " "junitxml doctest keyword").split() diff --git a/testing/test_funcargs.py b/testing/plugin/test_pytest_python.py similarity index 53% rename from testing/test_funcargs.py rename to testing/plugin/test_pytest_python.py index 5b90ffe4f..cfc5a054f 100644 --- a/testing/test_funcargs.py +++ b/testing/plugin/test_pytest_python.py @@ -1,5 +1,521 @@ import py, sys -from py._test import funcargs +from py._plugin import pytest_python as funcargs + +class TestModule: + def test_module_file_not_found(self, testdir): + tmpdir = testdir.tmpdir + fn = tmpdir.join('nada','no') + col = py.test.collect.Module(fn, config=testdir.Config()) + col.config = testdir.parseconfig(tmpdir) + py.test.raises(py.error.ENOENT, col.collect) + + def test_failing_import(self, testdir): + modcol = testdir.getmodulecol("import alksdjalskdjalkjals") + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.run) + + def test_import_duplicate(self, testdir): + a = testdir.mkdir("a") + b = testdir.mkdir("b") + p = a.ensure("test_whatever.py") + p.pyimport() + del py.std.sys.modules['test_whatever'] + b.ensure("test_whatever.py") + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*import*mismatch*", + "*imported*test_whatever*", + "*%s*" % a.join("test_whatever.py"), + "*not the same*", + "*%s*" % b.join("test_whatever.py"), + "*HINT*", + ]) + + def test_syntax_error_in_module(self, testdir): + modcol = testdir.getmodulecol("this is a syntax error") + py.test.raises(modcol.CollectError, modcol.collect) + py.test.raises(modcol.CollectError, modcol.collect) + py.test.raises(modcol.CollectError, modcol.run) + + def test_module_considers_pluginmanager_at_import(self, testdir): + modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") + py.test.raises(ImportError, "modcol.obj") + +class TestClass: + def test_class_with_init_not_collected(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass1: + def __init__(self): + pass + class TestClass2(object): + def __init__(self): + pass + """) + l = modcol.collect() + assert len(l) == 0 + +if py.std.sys.version_info > (3, 0): + _func_name_attr = "__name__" +else: + _func_name_attr = "func_name" + +class TestGenerator: + def test_generative_functions(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + + def test_generative_methods(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == '[0]' + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + + def test_generative_functions_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield "seventeen", func1, 17, 3*5 + yield "fortytwo", func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['seventeen']" + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + assert gencolitems[1].name == "['fortytwo']" + assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' + + def test_generative_functions_unique_explicit_names(self, testdir): + # generative + modcol = testdir.getmodulecol(""" + def func(): pass + def test_gen(): + yield "name", func + yield "name", func + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, py.test.collect.Generator) + py.test.raises(ValueError, "gencol.collect()") + + def test_generative_methods_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield "m1", func1, 17, 3*5 + yield "m2", func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, py.test.collect.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], py.test.collect.Function) + assert isinstance(gencolitems[1], py.test.collect.Function) + assert gencolitems[0].name == "['m1']" + assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' + assert gencolitems[1].name == "['m2']" + assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' + + def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): + o = testdir.makepyfile(""" + def test_generative_order_of_execution(): + import py + test_list = [] + expected_list = list(range(6)) + + def list_append(item): + test_list.append(item) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + for i in expected_list: + yield list_append, i + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 7 + assert not skipped and not failed + + def test_order_of_execution_generator_different_codeline(self, testdir): + o = testdir.makepyfile(""" + def test_generative_tests_different_codeline(): + import py + test_list = [] + expected_list = list(range(3)) + + def list_append_2(): + test_list.append(2) + + def list_append_1(): + test_list.append(1) + + def list_append_0(): + test_list.append(0) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + yield list_append_0 + yield list_append_1 + yield list_append_2 + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 4 + assert not skipped and not failed + +class TestFunction: + def test_getmodulecollector(self, testdir): + item = testdir.getitem("def test_func(): pass") + modcol = item.getparent(py.test.collect.Module) + assert isinstance(modcol, py.test.collect.Module) + assert hasattr(modcol.obj, 'test_func') + + def test_function_equality(self, testdir, tmpdir): + config = testdir.reparseconfig() + f1 = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + f2 = py.test.collect.Function(name="name",config=config, + args=(1,), callobj=py.builtin.callable) + assert not f1 == f2 + assert f1 != f2 + f3 = py.test.collect.Function(name="name", config=config, + args=(1,2), callobj=py.builtin.callable) + assert not f3 == f2 + assert f3 != f2 + + assert not f3 == f1 + assert f3 != f1 + + f1_b = py.test.collect.Function(name="name", config=config, + args=(1,), callobj=isinstance) + assert f1 == f1_b + assert not f1 != f1_b + + def test_function_equality_with_callspec(self, testdir, tmpdir): + config = testdir.reparseconfig() + class callspec1: + param = 1 + funcargs = {} + id = "hello" + class callspec2: + param = 1 + funcargs = {} + id = "world" + collection = object() + f5 = py.test.collect.Function(name="name", config=config, + callspec=callspec1, callobj=isinstance, collection=collection) + f5b = py.test.collect.Function(name="name", config=config, + callspec=callspec2, callobj=isinstance, collection=collection) + assert f5 != f5b + assert not (f5 == f5b) + + def test_pyfunc_call(self, testdir): + item = testdir.getitem("def test_func(): raise ValueError") + config = item.config + class MyPlugin1: + def pytest_pyfunc_call(self, pyfuncitem): + raise ValueError + class MyPlugin2: + def pytest_pyfunc_call(self, pyfuncitem): + return True + config.pluginmanager.register(MyPlugin1()) + config.pluginmanager.register(MyPlugin2()) + config.hook.pytest_pyfunc_call(pyfuncitem=item) + +class TestSorting: + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = modcol.collect_by_name("test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = modcol.collect_by_name("test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = modcol.collect_by_name("test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_allow_sane_sorting_for_decorators(self, testdir): + modcol = testdir.getmodulecol(""" + def dec(f): + g = lambda: f(2) + g.place_as = f + return g + + + def test_b(y): + pass + test_b = dec(test_b) + + def test_a(y): + pass + test_a = dec(test_a) + """) + colitems = modcol.collect() + assert len(colitems) == 2 + assert [item.name for item in colitems] == ['test_b', 'test_a'] + + +class TestConftestCustomization: + def test_pytest_pycollect_module(self, testdir): + testdir.makeconftest(""" + import py + class MyModule(py.test.collect.Module): + pass + def pytest_pycollect_makemodule(path, parent): + if path.basename == "test_xyz.py": + return MyModule(path, parent) + """) + testdir.makepyfile("def some(): pass") + testdir.makepyfile(test_xyz="") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*3 + + def test_traceback_error_during_import(self, testdir): + testdir.makepyfile(""" + x = 1 + x = 2 + x = 17 + asd + """) + result = testdir.runpytest() + assert result.ret != 0 + out = result.stdout.str() + assert "x = 1" not in out + assert "x = 2" not in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) + result = testdir.runpytest("--fulltrace") + out = result.stdout.str() + assert "x = 1" in out + assert "x = 2" in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) def test_getfuncargnames(): def f(): pass diff --git a/testing/plugin/test_pytest_tmpdir.py b/testing/plugin/test_pytest_tmpdir.py index 692956f45..d19088d91 100644 --- a/testing/plugin/test_pytest_tmpdir.py +++ b/testing/plugin/test_pytest_tmpdir.py @@ -1,7 +1,7 @@ from py._plugin.pytest_tmpdir import pytest_funcarg__tmpdir +from py._plugin.pytest_python import FuncargRequest def test_funcarg(testdir): - from py._test.funcargs import FuncargRequest item = testdir.getitem("def test_func(tmpdir): pass") p = pytest_funcarg__tmpdir(FuncargRequest(item)) assert p.check() diff --git a/testing/test_pycollect.py b/testing/test_pycollect.py deleted file mode 100644 index bc5a8b068..000000000 --- a/testing/test_pycollect.py +++ /dev/null @@ -1,517 +0,0 @@ -import py - -class TestModule: - def test_module_file_not_found(self, testdir): - tmpdir = testdir.tmpdir - fn = tmpdir.join('nada','no') - col = py.test.collect.Module(fn, config=testdir.Config()) - col.config = testdir.parseconfig(tmpdir) - py.test.raises(py.error.ENOENT, col.collect) - - def test_failing_import(self, testdir): - modcol = testdir.getmodulecol("import alksdjalskdjalkjals") - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.run) - - def test_import_duplicate(self, testdir): - a = testdir.mkdir("a") - b = testdir.mkdir("b") - p = a.ensure("test_whatever.py") - p.pyimport() - del py.std.sys.modules['test_whatever'] - b.ensure("test_whatever.py") - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*import*mismatch*", - "*imported*test_whatever*", - "*%s*" % a.join("test_whatever.py"), - "*not the same*", - "*%s*" % b.join("test_whatever.py"), - "*HINT*", - ]) - - def test_syntax_error_in_module(self, testdir): - modcol = testdir.getmodulecol("this is a syntax error") - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.run) - - def test_module_considers_pluginmanager_at_import(self, testdir): - modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") - py.test.raises(ImportError, "modcol.obj") - -class TestClass: - def test_class_with_init_not_collected(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass1: - def __init__(self): - pass - class TestClass2(object): - def __init__(self): - pass - """) - l = modcol.collect() - assert len(l) == 0 - -if py.std.sys.version_info > (3, 0): - _func_name_attr = "__name__" -else: - _func_name_attr = "func_name" - -class TestGenerator: - def test_generative_functions(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - - def test_generative_methods(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == '[0]' - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - - def test_generative_functions_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield "seventeen", func1, 17, 3*5 - yield "fortytwo", func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['seventeen']" - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - assert gencolitems[1].name == "['fortytwo']" - assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' - - def test_generative_functions_unique_explicit_names(self, testdir): - # generative - modcol = testdir.getmodulecol(""" - def func(): pass - def test_gen(): - yield "name", func - yield "name", func - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) - py.test.raises(ValueError, "gencol.collect()") - - def test_generative_methods_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield "m1", func1, 17, 3*5 - yield "m2", func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) - assert gencolitems[0].name == "['m1']" - assert getattr(gencolitems[0].obj, _func_name_attr) == 'func1' - assert gencolitems[1].name == "['m2']" - assert getattr(gencolitems[1].obj, _func_name_attr) == 'func1' - - def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): - o = testdir.makepyfile(""" - def test_generative_order_of_execution(): - import py - test_list = [] - expected_list = list(range(6)) - - def list_append(item): - test_list.append(item) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - for i in expected_list: - yield list_append, i - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 7 - assert not skipped and not failed - - def test_order_of_execution_generator_different_codeline(self, testdir): - o = testdir.makepyfile(""" - def test_generative_tests_different_codeline(): - import py - test_list = [] - expected_list = list(range(3)) - - def list_append_2(): - test_list.append(2) - - def list_append_1(): - test_list.append(1) - - def list_append_0(): - test_list.append(0) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - yield list_append_0 - yield list_append_1 - yield list_append_2 - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 4 - assert not skipped and not failed - -class TestFunction: - def test_getmodulecollector(self, testdir): - item = testdir.getitem("def test_func(): pass") - modcol = item.getparent(py.test.collect.Module) - assert isinstance(modcol, py.test.collect.Module) - assert hasattr(modcol.obj, 'test_func') - - def test_function_equality(self, testdir, tmpdir): - config = testdir.reparseconfig() - f1 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - f2 = py.test.collect.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable) - assert not f1 == f2 - assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable) - assert not f3 == f2 - assert f3 != f2 - - assert not f3 == f1 - assert f3 != f1 - - f1_b = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance) - assert f1 == f1_b - assert not f1 != f1_b - - def test_function_equality_with_callspec(self, testdir, tmpdir): - config = testdir.reparseconfig() - class callspec1: - param = 1 - funcargs = {} - id = "hello" - class callspec2: - param = 1 - funcargs = {} - id = "world" - collection = object() - f5 = py.test.collect.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance, collection=collection) - f5b = py.test.collect.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance, collection=collection) - assert f5 != f5b - assert not (f5 == f5b) - - def test_pyfunc_call(self, testdir): - item = testdir.getitem("def test_func(): raise ValueError") - config = item.config - class MyPlugin1: - def pytest_pyfunc_call(self, pyfuncitem): - raise ValueError - class MyPlugin2: - def pytest_pyfunc_call(self, pyfuncitem): - return True - config.pluginmanager.register(MyPlugin1()) - config.pluginmanager.register(MyPlugin2()) - config.hook.pytest_pyfunc_call(pyfuncitem=item) - -class TestSorting: - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = modcol.collect_by_name("test_pass") - assert isinstance(fn1, py.test.collect.Function) - fn2 = modcol.collect_by_name("test_pass") - assert isinstance(fn2, py.test.collect.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = modcol.collect_by_name("test_fail") - assert isinstance(fn3, py.test.collect.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - for fn in fn1,fn2,fn3: - assert fn != 3 - assert fn != modcol - assert fn != [1,2,3] - assert [1,2,3] != fn - assert modcol != fn - - def test_allow_sane_sorting_for_decorators(self, testdir): - modcol = testdir.getmodulecol(""" - def dec(f): - g = lambda: f(2) - g.place_as = f - return g - - - def test_b(y): - pass - test_b = dec(test_b) - - def test_a(y): - pass - test_a = dec(test_a) - """) - colitems = modcol.collect() - assert len(colitems) == 2 - assert [item.name for item in colitems] == ['test_b', 'test_a'] - - -class TestConftestCustomization: - def test_pytest_pycollect_module(self, testdir): - testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): - pass - def pytest_pycollect_makemodule(path, parent): - if path.basename == "test_xyz.py": - return MyModule(path, parent) - """) - testdir.makepyfile("def some(): pass") - testdir.makepyfile(test_xyz="") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*3 - - def test_traceback_error_during_import(self, testdir): - testdir.makepyfile(""" - x = 1 - x = 2 - x = 17 - asd - """) - result = testdir.runpytest() - assert result.ret != 0 - out = result.stdout.str() - assert "x = 1" not in out - assert "x = 2" not in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) - result = testdir.runpytest("--fulltrace") - out = result.stdout.str() - assert "x = 1" in out - assert "x = 2" in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) From 7d1585215df4cb629b94c3f247dff5ef213b6251 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 26 Sep 2010 16:23:43 +0200 Subject: [PATCH 26/38] clean up and simplify startup test protocols and objects introduce some new experimental hooks pytest_runtest_mainloop to better integrate distributed testing --HG-- branch : trunk --- py/_plugin/hookspec.py | 12 +- py/_plugin/pytest_default.py | 34 +++--- py/_plugin/pytest_genscript.py | 4 +- py/_plugin/pytest_helpconfig.py | 15 +-- py/_plugin/pytest_keyword.py | 7 +- py/_plugin/pytest_pytester.py | 4 +- py/_plugin/pytest_terminal.py | 25 ++-- py/_test/pluginmanager.py | 11 +- py/_test/session.py | 149 ++++++++++-------------- testing/plugin/test_pytest_genscript.py | 2 +- testing/test_collection.py | 2 +- testing/test_pluginmanager.py | 13 +++ 12 files changed, 141 insertions(+), 137 deletions(-) diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 925d4a5aa..b83fb249f 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -24,6 +24,10 @@ def pytest_cmdline_main(config): """ called for performing the main (cmdline) action. """ pytest_cmdline_main.firstresult = True +def pytest_runtest_mainloop(session): + """ called for performing the main runtest loop (after collection. """ +pytest_runtest_mainloop.firstresult = True + def pytest_unconfigure(config): """ called before test process is exited. """ @@ -31,10 +35,11 @@ def pytest_unconfigure(config): # collection hooks # ------------------------------------------------------------------------- -def pytest_log_startcollection(collection): - """ called before collection.perform_collection() is called. """ +def pytest_perform_collection(session): + """ perform the collection protocol for the given session. """ +pytest_perform_collection.firstresult = True -def pytest_collection_modifyitems(collection): +def pytest_collection_modifyitems(config, items): """ called to allow filtering and selecting of test items (inplace). """ def pytest_log_finishcollection(collection): @@ -139,6 +144,7 @@ def pytest_sessionstart(session): def pytest_sessionfinish(session, exitstatus): """ whole test run finishes. """ + # ------------------------------------------------------------------------- # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_default.py b/py/_plugin/pytest_default.py index 45f13376a..25e388d0f 100644 --- a/py/_plugin/pytest_default.py +++ b/py/_plugin/pytest_default.py @@ -4,15 +4,26 @@ import sys import py def pytest_cmdline_main(config): - from py._test.session import Session, Collection - collection = Collection(config) - # instantiate session already because it - # records failures and implements maxfail handling - session = Session(config, collection) - exitstatus = collection.do_collection() - if not exitstatus: - exitstatus = session.main() - return exitstatus + from py._test.session import Session + return Session(config).main() + +def pytest_perform_collection(session): + collection = session.collection + assert not hasattr(collection, 'items') + hook = session.config.hook + collection.items = items = collection.perform_collect() + hook.pytest_collection_modifyitems(config=session.config, items=items) + hook.pytest_log_finishcollection(collection=collection) + return True + +def pytest_runtest_mainloop(session): + if session.config.option.collectonly: + return True + for item in session.collection.items: + item.config.hook.pytest_runtest_protocol(item=item) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True def pytest_ignore_collect(path, config): ignore_paths = config.getconftest_pathlist("collect_ignore", path=path) @@ -21,11 +32,6 @@ def pytest_ignore_collect(path, config): if excludeopt: ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths - # XXX more refined would be: - if ignore_paths: - for p in ignore_paths: - if path == p or path.relto(p): - return True def pytest_collect_directory(path, parent): # XXX reconsider the following comment diff --git a/py/_plugin/pytest_genscript.py b/py/_plugin/pytest_genscript.py index 3cb710c0c..bd72aadc5 100755 --- a/py/_plugin/pytest_genscript.py +++ b/py/_plugin/pytest_genscript.py @@ -11,7 +11,7 @@ def pytest_addoption(parser): dest="genscript", metavar="path", help="create standalone py.test script at given target path.") -def pytest_configure(config): +def pytest_cmdline_main(config): genscript = config.getvalue("genscript") if genscript: import py @@ -20,7 +20,7 @@ def pytest_configure(config): pybasedir = py.path.local(py.__file__).dirpath().dirpath() genscript = py.path.local(genscript) main(pybasedir, outfile=genscript, infile=infile) - raise SystemExit(0) + return 0 def main(pybasedir, outfile, infile): import base64 diff --git a/py/_plugin/pytest_helpconfig.py b/py/_plugin/pytest_helpconfig.py index 3bb51bbcc..18c810f70 100644 --- a/py/_plugin/pytest_helpconfig.py +++ b/py/_plugin/pytest_helpconfig.py @@ -23,15 +23,18 @@ def pytest_addoption(parser): help="show available conftest.py and ENV-variable names.") -def pytest_configure(__multicall__, config): +def pytest_cmdline_main(config): if config.option.version: p = py.path.local(py.__file__).dirpath() sys.stderr.write("This is py.test version %s, imported from %s\n" % (py.__version__, p)) - sys.exit(0) - if not config.option.helpconfig: - return - __multicall__.execute() + return 0 + elif config.option.helpconfig: + config.pluginmanager.do_configure(config) + showpluginhelp(config) + return 0 + +def showpluginhelp(config): options = [] for group in config._parser._groups: options.extend(group.options) @@ -65,9 +68,7 @@ def pytest_configure(__multicall__, config): help, ) tw.line(line[:tw.fullwidth]) - tw.sep("-") - sys.exit(0) conftest_options = ( ('pytest_plugins', 'list of plugin names to load'), diff --git a/py/_plugin/pytest_keyword.py b/py/_plugin/pytest_keyword.py index 35ee6cd81..68d0f5d03 100644 --- a/py/_plugin/pytest_keyword.py +++ b/py/_plugin/pytest_keyword.py @@ -8,8 +8,7 @@ def pytest_addoption(parser): "Terminate the expression with ':' to treat a match as a signal " "to run all subsequent tests. ") -def pytest_collection_modifyitems(collection): - config = collection.config +def pytest_collection_modifyitems(items, config): keywordexpr = config.option.keyword if not keywordexpr: return @@ -20,7 +19,7 @@ def pytest_collection_modifyitems(collection): remaining = [] deselected = [] - for colitem in collection.items: + for colitem in items: if keywordexpr and skipbykeyword(colitem, keywordexpr): deselected.append(colitem) else: @@ -30,7 +29,7 @@ def pytest_collection_modifyitems(collection): if deselected: config.hook.pytest_deselected(items=deselected) - collection.items[:] = remaining + items[:] = remaining def skipbykeyword(colitem, keywordexpr): """ return True if they given keyword expression means to diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index 18df368c5..5b4f965e3 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -193,9 +193,9 @@ class TmpTestdir: args = ("-s", ) + args # otherwise FD leakage config = self.parseconfig(*args) reprec = self.getreportrecorder(config) - config.pluginmanager.do_configure(config) + #config.pluginmanager.do_configure(config) config.hook.pytest_cmdline_main(config=config) - config.pluginmanager.do_unconfigure(config) + #config.pluginmanager.do_unconfigure(config) return reprec def config_preparse(self): diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index bed5630e3..68fed6284 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -29,21 +29,13 @@ def pytest_addoption(parser): help="don't cut any tracebacks (default is to cut).") def pytest_configure(config): + if config.option.showfuncargs: + return if config.option.collectonly: reporter = CollectonlyReporter(config) - elif config.option.showfuncargs: - reporter = None else: reporter = TerminalReporter(config) - if reporter: - # XXX see remote.py's XXX - for attr in 'pytest_terminal_hasmarkup', 'pytest_terminal_fullwidth': - if hasattr(config, attr): - #print "SETTING TERMINAL OPTIONS", attr, getattr(config, attr) - name = attr.split("_")[-1] - assert hasattr(self.reporter._tw, name), name - setattr(reporter._tw, name, getattr(config, attr)) - config.pluginmanager.register(reporter, 'terminalreporter') + config.pluginmanager.register(reporter, 'terminalreporter') def getreportopt(config): reportopts = "" @@ -192,7 +184,10 @@ class TerminalReporter: markup = {} self.stats.setdefault(cat, []).append(rep) if not self.config.option.verbose: - self.write_fspath_result(self._getfspath(rep.item), letter) + fspath = getattr(rep, 'fspath', None) + if not fspath: + fspath = self._getfspath(rep.item) + self.write_fspath_result(fspath, letter) else: line = self._reportinfoline(rep.item) if not hasattr(rep, 'node'): @@ -217,17 +212,19 @@ class TerminalReporter: def pytest_sessionstart(self, session): self.write_sep("=", "test session starts", bold=True) self._sessionstarttime = py.std.time.time() - verinfo = ".".join(map(str, sys.version_info[:3])) msg = "platform %s -- Python %s" % (sys.platform, verinfo) msg += " -- pytest-%s" % (py.__version__) - if self.config.option.verbose or self.config.option.debug or getattr(self.config.option, 'pastebin', None): + if self.config.option.verbose or self.config.option.debug or \ + getattr(self.config.option, 'pastebin', None): msg += " -- " + str(sys.executable) self.write_line(msg) lines = self.config.hook.pytest_report_header(config=self.config) lines.reverse() for line in flatten(lines): self.write_line(line) + + def pytest_log_finishcollection(self): for i, testarg in enumerate(self.config.args): self.write_line("test path %d: %s" %(i+1, testarg)) diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index 32abedf65..beea192a1 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -32,7 +32,7 @@ class PluginManager(object): name = id(plugin) return name - def register(self, plugin, name=None): + def register(self, plugin, name=None, prepend=False): assert not self.isregistered(plugin), plugin assert not self.registry.isregistered(plugin), plugin name = self._getpluginname(plugin, name) @@ -41,7 +41,7 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.registry.register(plugin) + self.registry.register(plugin, prepend=prepend) return True def unregister(self, plugin): @@ -277,10 +277,13 @@ class Registry: plugins = [] self._plugins = plugins - def register(self, plugin): + def register(self, plugin, prepend=False): assert not isinstance(plugin, str) assert not plugin in self._plugins - self._plugins.append(plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) def unregister(self, plugin): self._plugins.remove(plugin) diff --git a/py/_test/session.py b/py/_test/session.py index d65a8ab30..57bd52036 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -6,7 +6,7 @@ """ import py -import sys +import os, sys # # main entry point @@ -16,11 +16,9 @@ def main(args=None): if args is None: args = sys.argv[1:] config = py.test.config + config.parse(args) try: - config.parse(args) - config.pluginmanager.do_configure(config) exitstatus = config.hook.pytest_cmdline_main(config=config) - config.pluginmanager.do_unconfigure(config) except config.Error: e = sys.exc_info()[1] sys.stderr.write("ERROR: %s\n" %(e.args[0],)) @@ -28,7 +26,6 @@ def main(args=None): py.test.config = py.test.config.__class__() return exitstatus - # exitcodes for the command line EXIT_OK = 0 EXIT_TESTSFAILED = 1 @@ -36,27 +33,25 @@ EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 EXIT_NOHOSTS = 4 -# imports used for genitems() -Item = py.test.collect.Item -Collector = py.test.collect.Collector - class Session(object): nodeid = "" class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ __module__ = 'builtins' # for py3 - def __init__(self, config, collection): + def __init__(self, config): self.config = config - self.pluginmanager = config.pluginmanager # shortcut - self.pluginmanager.register(self) + self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False - self.collection = collection + self.collection = Collection(config) - def sessionstarts(self): - """ setup any neccessary resources ahead of the test run. """ - self.config.hook.pytest_sessionstart(session=self) + def sessionfinishes(self, exitstatus): + # XXX move to main loop / refactor mainloop + self.config.hook.pytest_sessionfinish( + session=self, + exitstatus=exitstatus, + ) def pytest_runtest_logreport(self, report): if report.failed: @@ -68,24 +63,22 @@ class Session(object): self.collection.shouldstop = self.shouldstop pytest_collectreport = pytest_runtest_logreport - def sessionfinishes(self, exitstatus): - """ teardown any resources after a test run. """ - self.config.hook.pytest_sessionfinish( - session=self, - exitstatus=exitstatus, - ) - def main(self): """ main loop for running tests. """ self.shouldstop = False - - self.sessionstarts() exitstatus = EXIT_OK + config = self.config try: - self._mainloop() + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=self) + config.hook.pytest_perform_collection(session=self) + config.hook.pytest_runtest_mainloop(session=self) if self._testsfailed: exitstatus = EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus) + config.pluginmanager.do_unconfigure(config) + except self.config.Error: + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) @@ -94,19 +87,12 @@ class Session(object): excinfo = py.code.ExceptionInfo() self.config.pluginmanager.notify_exception(excinfo) exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") if exitstatus in (EXIT_INTERNALERROR, EXIT_INTERRUPTED): self.sessionfinishes(exitstatus=exitstatus) return exitstatus - def _mainloop(self): - if self.config.option.collectonly: - return - for item in self.collection.items: - item.config.hook.pytest_runtest_protocol(item=item) - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - class Collection: def __init__(self, config): self.config = config @@ -121,13 +107,15 @@ class Collection: def _normalizearg(self, arg): return "::".join(self._parsearg(arg)) - def _parsearg(self, arg): + def _parsearg(self, arg, base=None): """ return normalized name list for a command line specified id which might be of the form x/y/z::name1::name2 and should result into the form x::y::z::name1::name2 """ + if base is None: + base = py.path.local() parts = str(arg).split("::") - path = py.path.local(parts[0]) + path = base.join(parts[0], abs=True) if not path.check(): raise self.config.Error("file not found: %s" %(path,)) topdir = self.topdir @@ -137,17 +125,21 @@ class Collection: topparts = path.relto(topdir).split(path.sep) return topparts + parts[1:] - def getid(self, node, relative=True): + def getid(self, node): """ return id for node, relative to topdir. """ path = node.fspath chain = [x for x in node.listchain() if x.fspath == path] chain = chain[1:] names = [x.name for x in chain if x.name != "()"] - if relative: - relpath = path.relto(self.topdir) - if relpath: - path = relpath - names = relpath.split(node.fspath.sep) + names + relpath = path.relto(self.topdir) + if not relpath: + assert path == self.topdir + path = '' + else: + path = relpath + if os.sep != "/": + path = str(path).replace(os.sep, "/") + names.insert(0, path) return "::".join(names) def getbyid(self, id): @@ -158,6 +150,9 @@ class Collection: names = id.split("::") while names: name = names.pop(0) + newnames = name.split("/") + name = newnames[0] + names[:0] = newnames[1:] l = [] for current in matching: for x in current._memocollect(): @@ -172,22 +167,6 @@ class Collection: matching = l return matching - def do_collection(self): - assert not hasattr(self, 'items') - hook = self.config.hook - hook.pytest_log_startcollection(collection=self) - try: - self.items = self.perform_collect() - except self.config.Error: - raise - except Exception: - self.config.pluginmanager.notify_exception() - return EXIT_INTERNALERROR - else: - hook.pytest_collection_modifyitems(collection=self) - res = hook.pytest_log_finishcollection(collection=self) - return res and max(res) or 0 # returncode - def getinitialnodes(self): idlist = [self._normalizearg(arg) for arg in self.config.args] nodes = [] @@ -206,36 +185,36 @@ class Collection: names = list(names) name = names and names.pop(0) or None for node in matching: - if isinstance(node, Item): + if isinstance(node, py.test.collect.Item): if name is None: self.config.hook.pytest_log_itemcollect(item=node) result.append(node) - else: - assert isinstance(node, Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - #print "matching", rep.result, "against name", name - if rep.passed: - if name: - matched = False - for subcol in rep.result: - if subcol.name != name and subcol.name == "()": - names.insert(0, name) - name = "()" - # see doctests/custom naming XXX - if subcol.name == name or subcol.fspath.basename == name: - self.genitems([subcol], names, result) - matched = True - if not matched: - raise self.config.Error( - "can't collect: %s" % (name,)) + continue + assert isinstance(node, py.test.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + #print "matching", rep.result, "against name", name + if rep.passed: + if name: + matched = False + for subcol in rep.result: + if subcol.name != name and subcol.name == "()": + names.insert(0, name) + name = "()" + # see doctests/custom naming XXX + if subcol.name == name or subcol.fspath.basename == name: + self.genitems([subcol], names, result) + matched = True + if not matched: + raise self.config.Error( + "can't collect: %s" % (name,)) - else: - self.genitems(rep.result, [], result) - node.ihook.pytest_collectreport(report=rep) - x = getattr(self, 'shouldstop', None) - if x: - raise self.Interrupted(x) + else: + self.genitems(rep.result, [], result) + node.ihook.pytest_collectreport(report=rep) + x = getattr(self, 'shouldstop', None) + if x: + raise Session.Interrupted(x) def gettopdir(args): """ return the top directory for the given paths. diff --git a/testing/plugin/test_pytest_genscript.py b/testing/plugin/test_pytest_genscript.py index a880f3904..10da2e91d 100644 --- a/testing/plugin/test_pytest_genscript.py +++ b/testing/plugin/test_pytest_genscript.py @@ -26,7 +26,7 @@ def test_gen(testdir, anypython, standalone): "*imported from*mypytest" ]) -@py.test.mark.xfail(reason="fix-dist") +@py.test.mark.xfail(reason="fix-dist", run=False) def test_rundist(testdir, pytestconfig, standalone): pytestconfig.pluginmanager.skipifmissing("xdist") testdir.makepyfile(""" diff --git a/testing/test_collection.py b/testing/test_collection.py index 255b8a16c..d80e7b19e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -145,7 +145,7 @@ class TestCollection: bbb = testdir.mkpydir("bbb") p.copy(aaa.join("test_aaa.py")) p.move(bbb.join("test_bbb.py")) - + id = "." config = testdir.parseconfig(id) rcol = Collection(config) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 8b705d7b3..dddf99d0b 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -160,6 +160,19 @@ class TestBootstrapping: pp.unregister(a2) assert not pp.isregistered(a2) + def test_registry_ordering(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + pp.register(a2, "hello") + l = pp.getplugins() + assert l.index(a1) < l.index(a2) + a3 = A() + pp.register(a3, prepend=True) + l = pp.getplugins() + assert l.index(a3) == 0 + def test_register_imported_modules(self): pp = PluginManager() mod = py.std.types.ModuleType("x.y.pytest_hello") From 1c020c3d32ebf0cd008271462b6bbc12a77db2d9 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 26 Sep 2010 16:23:44 +0200 Subject: [PATCH 27/38] shift reporting info generation away from terminal reporting time, simplify code. also get rid of redundant 'shortrepr' on collect/test reports and rename reportinfo to "location" in some places --HG-- branch : trunk --- ISSUES.txt | 7 + py/_plugin/hookspec.py | 6 +- py/_plugin/pytest_doctest.py | 3 + py/_plugin/pytest_junitxml.py | 12 +- py/_plugin/pytest_monkeypatch.py | 2 +- py/_plugin/pytest_pytester.py | 3 +- py/_plugin/pytest_resultlog.py | 21 ++- py/_plugin/pytest_runner.py | 226 ++++++++++++------------- py/_plugin/pytest_skipping.py | 23 ++- py/_plugin/pytest_terminal.py | 129 +++++--------- py/_test/collect.py | 1 - py/_test/pluginmanager.py | 6 +- py/_test/session.py | 3 + testing/plugin/test_pytest_hooklog.py | 2 +- testing/plugin/test_pytest_keyword.py | 4 +- testing/plugin/test_pytest_runner.py | 34 ++-- testing/plugin/test_pytest_skipping.py | 26 ++- testing/plugin/test_pytest_terminal.py | 24 +-- testing/test_collect.py | 9 +- testing/test_collection.py | 16 +- testing/test_session.py | 6 +- 21 files changed, 255 insertions(+), 308 deletions(-) diff --git a/ISSUES.txt b/ISSUES.txt index f585b00f1..4474b4a29 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,3 +1,10 @@ +checks / deprecations for next release +--------------------------------------------------------------- +tags: bug 1.4 core xdist + +* reportinfo -> location in hooks and items +* check oejskit plugin compatibility + refine session initialization / fix custom collect crash --------------------------------------------------------------- tags: bug 1.4 core xdist diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index b83fb249f..456c1ba9d 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -160,8 +160,10 @@ def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ def pytest_report_iteminfo(item): - """ return (fspath, lineno, name) for the item. - the information is used for result display and to sort tests + """ return (fspath, lineno, domainpath) for the item. + the information is used for result display and to sort tests. + fspath,lineno: file and linenumber of source of item definition. + domainpath: custom id - e.g. for python: dotted import address """ pytest_report_iteminfo.firstresult = True diff --git a/py/_plugin/pytest_doctest.py b/py/_plugin/pytest_doctest.py index 20f19b2a0..45ff0ac31 100644 --- a/py/_plugin/pytest_doctest.py +++ b/py/_plugin/pytest_doctest.py @@ -86,6 +86,9 @@ class DoctestItem(py.test.collect.Item): else: return super(DoctestItem, self).repr_failure(excinfo) + def reportinfo(self): + return self.fspath, None, "[doctest]" + class DoctestTextfile(DoctestItem): def runtest(self): if not self._deprecated_testexecution(): diff --git a/py/_plugin/pytest_junitxml.py b/py/_plugin/pytest_junitxml.py index fc1df8fc4..48b716f9d 100644 --- a/py/_plugin/pytest_junitxml.py +++ b/py/_plugin/pytest_junitxml.py @@ -37,12 +37,9 @@ class LogXML(object): self._durations = {} def _opentestcase(self, report): - if hasattr(report, 'item'): - node = report.item - else: - node = report.collector - d = {'time': self._durations.pop(node, "0")} - names = [x.replace(".py", "") for x in node.listnames() if x != "()"] + names = report.nodenames + d = {'time': self._durations.pop(names, "0")} + names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] if self.prefix: classnames.insert(0, self.prefix) @@ -122,11 +119,12 @@ class LogXML(object): self.append_skipped(report) def pytest_runtest_call(self, item, __multicall__): + names = tuple(item.listnames()) start = time.time() try: return __multicall__.execute() finally: - self._durations[item] = time.time() - start + self._durations[names] = time.time() - start def pytest_collectreport(self, report): if not report.passed: diff --git a/py/_plugin/pytest_monkeypatch.py b/py/_plugin/pytest_monkeypatch.py index 37fc02e8a..6dc3e59e0 100644 --- a/py/_plugin/pytest_monkeypatch.py +++ b/py/_plugin/pytest_monkeypatch.py @@ -53,7 +53,7 @@ you start monkeypatching after the undo call. .. _`monkeypatch blog post`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ """ -import py, os, sys +import os, sys def pytest_funcarg__monkeypatch(request): """The returned ``monkeypatch`` funcarg provides these diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index 5b4f965e3..1d2abd2f4 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -396,8 +396,7 @@ class ReportRecorder(object): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): - colitem = rep.getnode() - if not inamepart or inamepart in colitem.listnames(): + if not inamepart or inamepart in rep.nodenames: l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % diff --git a/py/_plugin/pytest_resultlog.py b/py/_plugin/pytest_resultlog.py index 7f9292075..09b9213a2 100644 --- a/py/_plugin/pytest_resultlog.py +++ b/py/_plugin/pytest_resultlog.py @@ -57,21 +57,20 @@ class ResultLog(object): self.config = config self.logfile = logfile # preferably line buffered - def write_log_entry(self, testpath, shortrepr, longrepr): - print_("%s %s" % (shortrepr, testpath), file=self.logfile) + def write_log_entry(self, testpath, lettercode, longrepr): + print_("%s %s" % (lettercode, testpath), file=self.logfile) for line in longrepr.splitlines(): print_(" %s" % line, file=self.logfile) - def log_outcome(self, node, shortrepr, longrepr): - testpath = generic_path(node) - self.write_log_entry(testpath, shortrepr, longrepr) + def log_outcome(self, report, lettercode, longrepr): + testpath = getattr(report, 'nodeid', None) + if testpath is None: + testpath = report.fspath + self.write_log_entry(testpath, lettercode, longrepr) def pytest_runtest_logreport(self, report): res = self.config.hook.pytest_report_teststatus(report=report) - if res is not None: - code = res[1] - else: - code = report.shortrepr + code = res[1] if code == 'x': longrepr = str(report.longrepr) elif code == 'X': @@ -82,7 +81,7 @@ class ResultLog(object): longrepr = str(report.longrepr) elif report.skipped: longrepr = str(report.longrepr.reprcrash.message) - self.log_outcome(report.item, code, longrepr) + self.log_outcome(report, code, longrepr) def pytest_collectreport(self, report): if not report.passed: @@ -92,7 +91,7 @@ class ResultLog(object): assert report.skipped code = "S" longrepr = str(report.longrepr.reprcrash) - self.log_outcome(report.collector, code, longrepr) + self.log_outcome(report, code, longrepr) def pytest_internalerror(self, excrepr): path = excrepr.reprcrash.path diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index 784c13402..733d9b6ce 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -3,6 +3,7 @@ collect and run test items and create reports. """ import py, sys +from py._code.code import TerminalRepr def pytest_namespace(): return { @@ -28,16 +29,6 @@ def pytest_sessionfinish(session, exitstatus): if rep: hook.pytest__teardown_final_logerror(report=rep) -def pytest_make_collect_report(collector): - result = excinfo = None - try: - result = collector._memocollect() - except KeyboardInterrupt: - raise - except: - excinfo = py.code.ExceptionInfo() - return CollectReport(collector, result, excinfo) - def pytest_runtest_protocol(item): runtestprotocol(item) return True @@ -57,9 +48,6 @@ def pytest_runtest_call(item): if not item._deprecated_testexecution(): item.runtest() -def pytest_runtest_makereport(item, call): - return ItemTestReport(item, call.excinfo, call.when) - def pytest_runtest_teardown(item): item.config._setupstate.teardown_exact(item) @@ -68,8 +56,7 @@ def pytest__teardown_final(session): if call.excinfo: ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) call.excinfo.traceback = ntraceback.filter() - rep = TeardownErrorReport(call.excinfo) - return rep + return TeardownErrorReport(call.excinfo) def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): @@ -80,6 +67,8 @@ def pytest_report_teststatus(report): return "skipped", "s", "SKIPPED" else: return "", "", "" + + # # Implementation @@ -115,123 +104,120 @@ class CallInfo: return "" % (self.when, status) class BaseReport(object): - def __init__(self): - self.headerlines = [] - def __repr__(self): - l = ["%s=%s" %(key, value) - for key, value in self.__dict__.items()] - return "<%s %s>" %(self.__class__.__name__, " ".join(l),) - - def _getcrashline(self): - try: - return str(self.longrepr.reprcrash) - except AttributeError: - try: - return str(self.longrepr)[:50] - except AttributeError: - return "" - def toterminal(self, out): - for line in self.headerlines: - out.line(line) longrepr = self.longrepr if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: out.line(str(longrepr)) -class CollectErrorRepr(BaseReport): + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + +def pytest_runtest_makereport(item, call): + location = item.ihook.pytest_report_iteminfo(item=item) + location = (str(location[0]), location[1], str(location[2])) + nodenames = tuple(item.listnames()) + nodeid = item.collection.getid(item) + fspath = item.fspath + when = call.when + keywords = dict([(x,1) for x in item.keywords]) + excinfo = call.excinfo + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + if not isinstance(excinfo, py.code.ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + longrepr = item._repr_failure_py(excinfo) + else: + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py(excinfo) + return TestReport(nodeid, nodenames, fspath, location, + keywords, outcome, longrepr, when) + +class TestReport(BaseReport): + def __init__(self, nodeid, nodenames, fspath, location, + keywords, outcome, longrepr, when): + self.nodeid = nodeid + self.nodenames = nodenames + self.fspath = fspath # where the test was collected + self.location = location + self.keywords = keywords + self.outcome = outcome + self.longrepr = longrepr + self.when = when + + def __repr__(self): + return "" % ( + self.nodeid, self.when, self.outcome) + +class TeardownErrorReport(BaseReport): + outcome = "failed" + when = "teardown" + def __init__(self, excinfo): + self.longrepr = excinfo.getrepr(funcargs=True) + +def pytest_make_collect_report(collector): + result = excinfo = None + try: + result = collector._memocollect() + except KeyboardInterrupt: + raise + except: + excinfo = py.code.ExceptionInfo() + nodenames = tuple(collector.listnames()) + nodeid = collector.collection.getid(collector) + fspath = str(collector.fspath) + reason = longrepr = None + if not excinfo: + outcome = "passed" + else: + if excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + reason = str(excinfo.value) + longrepr = collector._repr_failure_py(excinfo, "line") + else: + outcome = "failed" + errorinfo = collector.repr_failure(excinfo) + if not hasattr(errorinfo, "toterminal"): + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + return CollectReport(nodenames, nodeid, fspath, + outcome, longrepr, result, reason) + +class CollectReport(BaseReport): + def __init__(self, nodenames, nodeid, fspath, outcome, + longrepr, result, reason): + self.nodenames = nodenames + self.nodeid = nodeid + self.fspath = fspath + self.outcome = outcome + self.longrepr = longrepr + self.result = result + self.reason = reason + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "" % (self.nodeid, self.outcome) + +class CollectErrorRepr(TerminalRepr): def __init__(self, msg): - super(CollectErrorRepr, self).__init__() self.longrepr = msg def toterminal(self, out): out.line(str(self.longrepr), red=True) -class ItemTestReport(BaseReport): - failed = passed = skipped = False - - def __init__(self, item, excinfo=None, when=None): - super(ItemTestReport, self).__init__() - self.item = item - self.when = when - if item and when != "setup": - self.keywords = item.keywords - else: - # if we fail during setup it might mean - # we are not able to access the underlying object - # this might e.g. happen if we are unpickled - # and our parent collector did not collect us - # (because it e.g. skipped for platform reasons) - self.keywords = {} - if not excinfo: - self.passed = True - self.shortrepr = "." - else: - if not isinstance(excinfo, py.code.ExceptionInfo): - self.failed = True - shortrepr = "?" - longrepr = excinfo - elif excinfo.errisinstance(py.test.skip.Exception): - self.skipped = True - shortrepr = "s" - longrepr = self.item._repr_failure_py(excinfo) - else: - self.failed = True - shortrepr = self.item.shortfailurerepr - if self.when == "call": - longrepr = self.item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = self.item._repr_failure_py(excinfo) - shortrepr = shortrepr.lower() - self.shortrepr = shortrepr - self.longrepr = longrepr - - def __repr__(self): - status = (self.passed and "passed" or - self.skipped and "skipped" or - self.failed and "failed" or - "CORRUPT") - l = [repr(self.item.name), "when=%r" % self.when, "outcome %r" % status,] - if hasattr(self, 'node'): - l.append("txnode=%s" % self.node.gateway.id) - info = " " .join(map(str, l)) - return "" % info - - def getnode(self): - return self.item - -class CollectReport(BaseReport): - skipped = failed = passed = False - - def __init__(self, collector, result, excinfo=None): - super(CollectReport, self).__init__() - self.collector = collector - if not excinfo: - self.passed = True - self.result = result - else: - if excinfo.errisinstance(py.test.skip.Exception): - self.skipped = True - self.reason = str(excinfo.value) - self.longrepr = self.collector._repr_failure_py(excinfo, "line") - else: - self.failed = True - errorinfo = self.collector.repr_failure(excinfo) - if not hasattr(errorinfo, "toterminal"): - errorinfo = CollectErrorRepr(errorinfo) - self.longrepr = errorinfo - - def getnode(self): - return self.collector - -class TeardownErrorReport(BaseReport): - skipped = passed = False - failed = True - when = "teardown" - def __init__(self, excinfo): - super(TeardownErrorReport, self).__init__() - self.longrepr = excinfo.getrepr(funcargs=True) - class SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ def __init__(self): diff --git a/py/_plugin/pytest_skipping.py b/py/_plugin/pytest_skipping.py index 70b64aabd..6c01a4c9c 100644 --- a/py/_plugin/pytest_skipping.py +++ b/py/_plugin/pytest_skipping.py @@ -231,19 +231,16 @@ def pytest_runtest_makereport(__multicall__, item, call): if not item.config.getvalue("runxfail"): rep = __multicall__.execute() rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg - rep.skipped = True - rep.failed = False + rep.outcome = "skipped" return rep if call.when == "call": rep = __multicall__.execute() evalxfail = getattr(item, '_evalxfail') if not item.config.getvalue("runxfail") and evalxfail.istrue(): if call.excinfo: - rep.skipped = True - rep.failed = rep.passed = False + rep.outcome = "skipped" else: - rep.skipped = rep.passed = False - rep.failed = True + rep.outcome = "failed" rep.keywords['xfail'] = evalxfail.getexplanation() else: if 'xfail' in rep.keywords: @@ -275,9 +272,9 @@ def pytest_terminal_summary(terminalreporter): show_xfailed(terminalreporter, lines) elif char == "X": show_xpassed(terminalreporter, lines) - elif char == "f": + elif char in "fF": show_failed(terminalreporter, lines) - elif char == "s": + elif char in "sS": show_skipped(terminalreporter, lines) if lines: tr._tw.sep("=", "short test summary info") @@ -289,22 +286,24 @@ def show_failed(terminalreporter, lines): failed = terminalreporter.stats.get("failed") if failed: for rep in failed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid lines.append("FAIL %s" %(pos, )) def show_xfailed(terminalreporter, lines): xfailed = terminalreporter.stats.get("xfailed") if xfailed: for rep in xfailed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid reason = rep.keywords['xfail'] - lines.append("XFAIL %s %s" %(pos, reason)) + lines.append("XFAIL %s" % (pos,)) + if reason: + lines.append(" " + str(reason)) def show_xpassed(terminalreporter, lines): xpassed = terminalreporter.stats.get("xpassed") if xpassed: for rep in xpassed: - pos = terminalreporter.gettestid(rep.item) + pos = rep.nodeid reason = rep.keywords['xfail'] lines.append("XPASS %s %s" %(pos, reason)) diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index 68fed6284..d205e91a2 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -57,6 +57,17 @@ def getreportopt(config): reportopts += char return reportopts +def pytest_report_teststatus(report): + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + elif report.failed: + letter = "F" + if report.when != "call": + letter = "f" + return report.outcome, letter, report.outcome.upper() + class TerminalReporter: def __init__(self, config, file=None): self.config = config @@ -104,42 +115,6 @@ class TerminalReporter: self.ensure_newline() self._tw.sep(sep, title, **markup) - def getcategoryletterword(self, rep): - res = self.config.hook.pytest_report_teststatus(report=rep) - if res: - return res - for cat in 'skipped failed passed ???'.split(): - if getattr(rep, cat, None): - break - return cat, self.getoutcomeletter(rep), self.getoutcomeword(rep) - - def getoutcomeletter(self, rep): - return rep.shortrepr - - def getoutcomeword(self, rep): - if rep.passed: - return "PASS", dict(green=True) - elif rep.failed: - return "FAIL", dict(red=True) - elif rep.skipped: - return "SKIP" - else: - return "???", dict(red=True) - - def gettestid(self, item, relative=True): - fspath = item.fspath - chain = [x for x in item.listchain() if x.fspath == fspath] - chain = chain[1:] - names = [x.name for x in chain if x.name != "()"] - path = item.fspath - if relative: - relpath = path.relto(self.curdir) - if relpath: - path = relpath - names.insert(0, str(path)) - return "::".join(names) - - def pytest_internalerror(self, excrepr): for line in str(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) @@ -160,21 +135,23 @@ class TerminalReporter: def pytest_deselected(self, items): self.stats.setdefault('deselected', []).extend(items) - def pytest_itemstart(self, item, node=None): - if self.config.option.verbose: - line = self._reportinfoline(item) - self.write_ensure_prefix(line, "") - else: - # ensure that the path is printed before the - # 1st test of a module starts running - self.write_fspath_result(self._getfspath(item), "") + #def pytest_itemstart(self, item, node=None): + # if self.config.option.verbose: + # line = self._locationline(rep) + # self.write_ensure_prefix(line, "") + # else: + # # ensure that the path is printed before the + # # 1st test of a module starts running + # self.write_fspath_result(self._getfspath(item), "") def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) def pytest_runtest_logreport(self, report): rep = report - cat, letter, word = self.getcategoryletterword(rep) + res = self.config.hook.pytest_report_teststatus(report=rep) + cat, letter, word = res + self.stats.setdefault(cat, []).append(rep) if not letter and not word: # probably passed setup/teardown return @@ -182,14 +159,10 @@ class TerminalReporter: word, markup = word else: markup = {} - self.stats.setdefault(cat, []).append(rep) if not self.config.option.verbose: - fspath = getattr(rep, 'fspath', None) - if not fspath: - fspath = self._getfspath(rep.item) - self.write_fspath_result(fspath, letter) + self.write_fspath_result(rep.fspath, letter) else: - line = self._reportinfoline(rep.item) + line = self._locationline(rep) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) else: @@ -204,10 +177,10 @@ class TerminalReporter: if not report.passed: if report.failed: self.stats.setdefault("error", []).append(report) - self.write_fspath_result(report.collector.fspath, "E") + self.write_fspath_result(report.fspath, "E") elif report.skipped: self.stats.setdefault("skipped", []).append(report) - self.write_fspath_result(report.collector.fspath, "S") + self.write_fspath_result(report.fspath, "S") def pytest_sessionstart(self, session): self.write_sep("=", "test session starts", bold=True) @@ -253,14 +226,14 @@ class TerminalReporter: else: excrepr.reprcrash.toterminal(self._tw) - def _reportinfoline(self, item): - collect_fspath = self._getfspath(item) - fspath, lineno, msg = self._getreportinfo(item) - if fspath and fspath != collect_fspath: - fspath = "%s <- %s" % ( - self.curdir.bestrelpath(collect_fspath), - self.curdir.bestrelpath(fspath)) - elif fspath: + def _locationline(self, rep): + #collect_fspath = self._getfspath(item) + fspath, lineno, msg = rep.location + #if fspath and fspath != collect_fspath: + # fspath = "%s <- %s" % ( + # self.curdir.bestrelpath(collect_fspath), + # self.curdir.bestrelpath(fspath)) + if fspath: fspath = self.curdir.bestrelpath(fspath) if lineno is not None: lineno += 1 @@ -271,34 +244,24 @@ class TerminalReporter: elif fspath and lineno: line = "%(fspath)s:%(lineno)s %(extrapath)s" else: - line = "[noreportinfo]" + line = "[nolocation]" return line % locals() + " " def _getfailureheadline(self, rep): - if hasattr(rep, "collector"): - return str(rep.collector.fspath) - elif hasattr(rep, 'item'): - fspath, lineno, msg = self._getreportinfo(rep.item) - return msg + if hasattr(rep, 'location'): + fspath, lineno, domain = rep.location + return domain else: - return "test session" + return "test session" # XXX? - def _getreportinfo(self, item): + def _getcrashline(self, rep): try: - return item.__reportinfo + return str(rep.longrepr.reprcrash) except AttributeError: - pass - reportinfo = item.config.hook.pytest_report_iteminfo(item=item) - # cache on item - item.__reportinfo = reportinfo - return reportinfo - - def _getfspath(self, item): - try: - return item.fspath - except AttributeError: - fspath, lineno, msg = self._getreportinfo(item) - return fspath + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" # # summaries for sessionfinish @@ -310,7 +273,7 @@ class TerminalReporter: self.write_sep("=", "FAILURES") for rep in self.stats['failed']: if tbstyle == "line": - line = rep._getcrashline() + line = self._getcrashline(rep) self.write_line(line) else: msg = self._getfailureheadline(rep) diff --git a/py/_test/collect.py b/py/_test/collect.py index e2bd650db..6a94cf2ee 100644 --- a/py/_test/collect.py +++ b/py/_test/collect.py @@ -126,7 +126,6 @@ class Node(object): style=style) repr_failure = _repr_failure_py - shortfailurerepr = "F" class Collector(Node): """ diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index beea192a1..89bd995b3 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -6,9 +6,9 @@ import inspect from py._plugin import hookspec default_plugins = ( - "default python runner pdb capture mark terminal skipping tmpdir monkeypatch " - "recwarn pastebin unittest helpconfig nose assertion genscript " - "junitxml doctest keyword").split() + "default terminal python runner pdb capture mark skipping tmpdir monkeypatch " + "recwarn pastebin unittest helpconfig nose assertion genscript " + "junitxml doctest keyword").split() def check_old_use(mod, modname): clsname = modname[len('pytest_'):].capitalize() + "Plugin" diff --git a/py/_test/session.py b/py/_test/session.py index 57bd52036..2524972bd 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -182,6 +182,9 @@ class Collection: return nodes def genitems(self, matching, names, result): + if not matching: + assert not names + return result names = list(names) name = names and names.pop(0) or None for node in matching: diff --git a/testing/plugin/test_pytest_hooklog.py b/testing/plugin/test_pytest_hooklog.py index b55359ef7..4b11e69b4 100644 --- a/testing/plugin/test_pytest_hooklog.py +++ b/testing/plugin/test_pytest_hooklog.py @@ -8,5 +8,5 @@ def test_functional(testdir): testdir.runpytest("--hooklog=hook.log") s = testdir.tmpdir.join("hook.log").read() assert s.find("pytest_sessionstart") != -1 - assert s.find("ItemTestReport") != -1 + assert s.find("TestReport") != -1 assert s.find("sessionfinish") != -1 diff --git a/testing/plugin/test_pytest_keyword.py b/testing/plugin/test_pytest_keyword.py index 0ac94e444..2f97c28a3 100644 --- a/testing/plugin/test_pytest_keyword.py +++ b/testing/plugin/test_pytest_keyword.py @@ -64,7 +64,7 @@ class TestKeywordSelection: reprec = testdir.inline_run("-s", "-k", keyword, file_test) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 - assert failed[0].item.name == name + assert failed[0].nodeid.split("::")[-1] == name assert len(reprec.getcalls('pytest_deselected')) == 1 for keyword in ['test_one', 'est_on']: @@ -92,7 +92,7 @@ class TestKeywordSelection: py.builtin.print_("keyword", repr(keyword)) passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 1 - assert passed[0].item.name == "test_2" + assert passed[0].nodeid.endswith("test_2") dlist = reprec.getcalls("pytest_deselected") assert len(dlist) == 1 assert dlist[0].items[0].name == 'test_1' diff --git a/testing/plugin/test_pytest_runner.py b/testing/plugin/test_pytest_runner.py index 6716af23e..fe0bc3677 100644 --- a/testing/plugin/test_pytest_runner.py +++ b/testing/plugin/test_pytest_runner.py @@ -53,8 +53,8 @@ class BaseFunctionalTests: rep = reports[1] assert rep.passed assert not rep.failed - assert rep.shortrepr == "." - assert not hasattr(rep, 'longrepr') + assert rep.outcome == "passed" + assert not rep.longrepr def test_failfunction(self, testdir): reports = testdir.runitem(""" @@ -66,23 +66,8 @@ class BaseFunctionalTests: assert not rep.skipped assert rep.failed assert rep.when == "call" - assert isinstance(rep.longrepr, ReprExceptionInfo) - assert str(rep.shortrepr) == "F" - - def test_failfunction_customized_report(self, testdir, LineMatcher): - reports = testdir.runitem(""" - def test_func(): - assert 0 - """) - rep = reports[1] - rep.headerlines += ["hello world"] - tr = py.io.TerminalWriter(stringio=True) - rep.toterminal(tr) - val = tr.stringio.getvalue() - LineMatcher(val.split("\n")).fnmatch_lines([ - "*hello world", - "*def test_func():*" - ]) + assert rep.outcome == "failed" + #assert isinstance(rep.longrepr, ReprExceptionInfo) def test_skipfunction(self, testdir): reports = testdir.runitem(""" @@ -94,6 +79,7 @@ class BaseFunctionalTests: assert not rep.failed assert not rep.passed assert rep.skipped + assert rep.outcome == "skipped" #assert rep.skipped.when == "call" #assert rep.skipped.when == "call" #assert rep.skipped == "%sreason == "hello" @@ -150,8 +136,8 @@ class BaseFunctionalTests: assert not rep.passed assert rep.failed assert rep.when == "teardown" - assert rep.longrepr.reprcrash.lineno == 3 - assert rep.longrepr.reprtraceback.reprentries + #assert rep.longrepr.reprcrash.lineno == 3 + #assert rep.longrepr.reprtraceback.reprentries def test_custom_failure_repr(self, testdir): testdir.makepyfile(conftest=""" @@ -270,6 +256,10 @@ class TestCollectionReports: assert not rep.failed assert not rep.skipped assert rep.passed + locinfo = rep.location + assert locinfo[0] == col.fspath + assert not locinfo[1] + assert locinfo[2] == col.fspath res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" @@ -299,7 +289,7 @@ def test_callinfo(): assert "exc" in repr(ci) # design question: do we want general hooks in python files? -# following passes if withpy defaults to True in pycoll.PyObjMix._getplugins() +# then something like the following functional tests makes sense @py.test.mark.xfail def test_runtest_in_module_ordering(testdir): p1 = testdir.makepyfile(""" diff --git a/testing/plugin/test_pytest_skipping.py b/testing/plugin/test_pytest_skipping.py index 9f0958d37..73c855724 100644 --- a/testing/plugin/test_pytest_skipping.py +++ b/testing/plugin/test_pytest_skipping.py @@ -183,8 +183,10 @@ class TestXFail: """) result = testdir.runpytest(p, '--report=xfailed', ) result.stdout.fnmatch_lines([ - "*test_one*test_this*NOTRUN*noway", - "*test_one*test_this_true*NOTRUN*condition:*True*", + "*test_one*test_this*", + "*NOTRUN*noway", + "*test_one*test_this_true*", + "*NOTRUN*condition:*True*", "*1 passed*", ]) @@ -199,7 +201,8 @@ class TestXFail: """) result = testdir.runpytest(p, '--report=xfailed', ) result.stdout.fnmatch_lines([ - "*test_one*test_this*NOTRUN*hello", + "*test_one*test_this*", + "*NOTRUN*hello", "*1 xfailed*", ]) @@ -229,7 +232,8 @@ class TestXFail: ]) result = testdir.runpytest(p, "-rx") result.stdout.fnmatch_lines([ - "*XFAIL*test_this*reason:*hello*", + "*XFAIL*test_this*", + "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ @@ -252,7 +256,8 @@ class TestXFail: ]) result = testdir.runpytest(p, "-rx") result.stdout.fnmatch_lines([ - "*XFAIL*test_this*reason:*hello*", + "*XFAIL*test_this*", + "*reason:*hello*", ]) result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ @@ -286,7 +291,8 @@ class TestXFail: """) result = testdir.runpytest(p, '-rxX') result.stdout.fnmatch_lines([ - "*XFAIL*test_this*NOTRUN*", + "*XFAIL*test_this*", + "*NOTRUN*", ]) def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): @@ -360,7 +366,6 @@ def test_skipif_class(testdir): def test_skip_reasons_folding(): - from py._plugin import pytest_runner as runner from py._plugin.pytest_skipping import folded_skips class longrepr: class reprcrash: @@ -368,12 +373,15 @@ def test_skip_reasons_folding(): lineno = 3 message = "justso" - ev1 = runner.CollectReport(None, None) + class X: + pass + ev1 = X() ev1.when = "execute" ev1.skipped = True ev1.longrepr = longrepr - ev2 = runner.ItemTestReport(None, excinfo=longrepr) + ev2 = X() + ev2.longrepr = longrepr ev2.skipped = True l = folded_skips([ev1, ev2]) diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index dd63826ae..d6af1450c 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -89,22 +89,7 @@ class TestTerminal: assert lines[1].endswith("xy.py .") assert lines[2] == "hello world" - def test_testid(self, testdir, linecomp): - func,method = testdir.getitems(""" - def test_func(): - pass - class TestClass: - def test_method(self): - pass - """) - tr = TerminalReporter(func.config, file=linecomp.stringio) - id = tr.gettestid(func) - assert id.endswith("test_testid.py::test_func") - fspath = py.path.local(id.split("::")[0]) - assert fspath.check() - id = tr.gettestid(method) - assert id.endswith("test_testid.py::TestClass::test_method") - + @py.test.mark.xfail(reason="re-implement ItemStart events") def test_show_path_before_running_test(self, testdir, linecomp): item = testdir.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) @@ -114,6 +99,7 @@ class TestTerminal: "*test_show_path_before_running_test.py*" ]) + @py.test.mark.xfail(reason="re-implement ItemStart events") def test_itemreport_reportinfo(self, testdir, linecomp): testdir.makeconftest(""" import py @@ -130,6 +116,7 @@ class TestTerminal: "*ABCDE:43: custom*" ]) + @py.test.mark.xfail(reason="re-implement ItemStart events") def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): item = testdir.getitem("def test_func(): pass") class Plugin: @@ -144,6 +131,7 @@ class TestTerminal: "*FGHJ:43: custom*" ]) + @py.test.mark.xfail(reason="re-implement subclassing precision reporting") def test_itemreport_subclasses_show_subclassed_file(self, testdir): p1 = testdir.makepyfile(test_p1=""" class BaseTests: @@ -210,8 +198,8 @@ class TestCollectonly: linecomp.assert_contains_lines([ " ", ]) - rep.config.hook.pytest_collectreport( - report=runner.CollectReport(modcol, [], excinfo=None)) + report = rep.config.hook.pytest_make_collect_report(collector=modcol) + rep.config.hook.pytest_collectreport(report=report) assert rep.indent == indent def test_collectonly_skipped_module(self, testdir, linecomp): diff --git a/testing/test_collect.py b/testing/test_collect.py index 82b3b5cdd..7f67152df 100644 --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -140,7 +140,7 @@ class TestCollectPluginHookRelay: assert "world" in wascalled # make sure the directories do not get double-appended colreports = reprec.getreports("pytest_collectreport") - names = [rep.collector.name for rep in colreports] + names = [rep.nodenames[-1] for rep in colreports] assert names.count("hello") == 1 class TestPrunetraceback: @@ -180,6 +180,7 @@ class TestPrunetraceback: "*hello world*", ]) + @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") def test_collect_report_postprocessing(self, testdir): p = testdir.makepyfile(""" import not_exists @@ -226,11 +227,13 @@ class TestCustomConftests: testdir.mkdir("hello") testdir.makepyfile(test_world="#") reprec = testdir.inline_run(testdir.tmpdir) - names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] + names = [rep.nodenames[-1] + for rep in reprec.getreports("pytest_collectreport")] assert 'hello' not in names assert 'test_world.py' not in names reprec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] + names = [rep.nodenames[-1] + for rep in reprec.getreports("pytest_collectreport")] assert 'hello' in names assert 'test_world.py' in names diff --git a/testing/test_collection.py b/testing/test_collection.py index d80e7b19e..a346ad4e0 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -60,8 +60,8 @@ class TestCollection: ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == p"), - ("pytest_collectreport", "report.collector.fspath == topdir") + ("pytest_collectreport", "report.fspath == p"), + ("pytest_collectreport", "report.fspath == topdir") ]) def test_collect_protocol_method(self, testdir): @@ -115,9 +115,9 @@ class TestCollection: ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == p"), + ("pytest_collectreport", "report.fspath == p"), ("pytest_collectreport", - "report.collector.fspath == report.collector.collection.topdir") + "report.fspath == %r" % str(rcol.topdir)), ]) def test_collect_subdir_event_ordering(self, testdir): @@ -135,8 +135,8 @@ class TestCollection: ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == test_aaa"), - ("pytest_collectreport", "report.collector.fspath == aaa"), + ("pytest_collectreport", "report.fspath == test_aaa"), + ("pytest_collectreport", "report.fspath == aaa"), ]) def test_collect_two_commandline_args(self, testdir): @@ -156,10 +156,10 @@ class TestCollection: hookrec.hookrecorder.contains([ ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == aaa"), + ("pytest_collectreport", "report.fspath == aaa"), ("pytest_collectstart", "collector.fspath == bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.collector.fspath == bbb"), + ("pytest_collectreport", "report.fspath == bbb"), ]) def test_serialization_byid(self, testdir): diff --git a/testing/test_session.py b/testing/test_session.py index 5bdc51a3a..64d84b252 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -17,9 +17,9 @@ class SessionTests: assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].item.name == "test_one_one" - assert failed[1].item.name == "test_other" - assert failed[2].item.name == "test_two" + assert failed[0].nodenames[-1] == "test_one_one" + assert failed[1].nodenames[-1] == "test_other" + assert failed[2].nodenames[-1] == "test_two" itemstarted = reprec.getcalls("pytest_log_itemcollect") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") From a2fe6714f860dfffb657d423355d6c644ccb7550 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 26 Sep 2010 16:23:45 +0200 Subject: [PATCH 28/38] implement pytest_runtest_logstart(nodeid, location) hook factor out a NodeInfo helper, and streamline terminal printing a bit --HG-- branch : trunk --- py/_plugin/hookspec.py | 8 +++-- py/_plugin/pytest_runner.py | 33 ++++++++++++++---- py/_plugin/pytest_terminal.py | 38 ++++++++++---------- testing/plugin/test_pytest_python.py | 25 ++++++++++++++ testing/plugin/test_pytest_terminal.py | 48 ++++++++------------------ 5 files changed, 90 insertions(+), 62 deletions(-) diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 456c1ba9d..f9ec47ada 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -102,14 +102,16 @@ def pytest_generate_tests(metafunc): # ------------------------------------------------------------------------- # generic runtest related hooks # ------------------------------------------------------------------------- - def pytest_itemstart(item, node=None): - """ test item starts running. """ + """ (deprecated, use pytest_runtest_logstart). """ def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True +def pytest_runtest_logstart(nodeid, location): + """ signal the start of a test run. """ + def pytest_runtest_setup(item): """ called before pytest_runtest_call(). """ @@ -160,7 +162,7 @@ def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ def pytest_report_iteminfo(item): - """ return (fspath, lineno, domainpath) for the item. + """ return (fspath, lineno, domainpath) location info for the item. the information is used for result display and to sort tests. fspath,lineno: file and linenumber of source of item definition. domainpath: custom id - e.g. for python: dotted import address diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index 733d9b6ce..a9989af2a 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -29,7 +29,31 @@ def pytest_sessionfinish(session, exitstatus): if rep: hook.pytest__teardown_final_logerror(report=rep) +class NodeInfo: + def __init__(self, nodeid, nodenames, fspath, location): + self.nodeid = nodeid + self.nodenames = nodenames + self.fspath = fspath + self.location = location + +def getitemnodeinfo(item): + try: + return item._nodeinfo + except AttributeError: + location = item.ihook.pytest_report_iteminfo(item=item) + location = (str(location[0]), location[1], str(location[2])) + nodenames = tuple(item.listnames()) + nodeid = item.collection.getid(item) + fspath = item.fspath + item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location) + return n + def pytest_runtest_protocol(item): + nodeinfo = getitemnodeinfo(item) + item.ihook.pytest_runtest_logstart( + nodeid=nodeinfo.nodeid, + location=nodeinfo.location + ) runtestprotocol(item) return True @@ -117,11 +141,7 @@ class BaseReport(object): def pytest_runtest_makereport(item, call): - location = item.ihook.pytest_report_iteminfo(item=item) - location = (str(location[0]), location[1], str(location[2])) - nodenames = tuple(item.listnames()) - nodeid = item.collection.getid(item) - fspath = item.fspath + nodeinfo = getitemnodeinfo(item) when = call.when keywords = dict([(x,1) for x in item.keywords]) excinfo = call.excinfo @@ -141,7 +161,8 @@ def pytest_runtest_makereport(item, call): longrepr = item.repr_failure(excinfo) else: # exception in setup or teardown longrepr = item._repr_failure_py(excinfo) - return TestReport(nodeid, nodenames, fspath, location, + return TestReport(nodeinfo.nodeid, nodeinfo.nodenames, + nodeinfo.fspath, nodeinfo.location, keywords, outcome, longrepr, when) class TestReport(BaseReport): diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index d205e91a2..db463d601 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -84,12 +84,12 @@ class TerminalReporter: return char in self.reportchars def write_fspath_result(self, fspath, res): - fspath = self.curdir.bestrelpath(fspath) if fspath != self.currentfspath: + self.currentfspath = fspath + fspath = self.curdir.bestrelpath(fspath) self._tw.line() relpath = self.curdir.bestrelpath(fspath) self._tw.write(relpath + " ") - self.currentfspath = fspath self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): @@ -135,18 +135,18 @@ class TerminalReporter: def pytest_deselected(self, items): self.stats.setdefault('deselected', []).extend(items) - #def pytest_itemstart(self, item, node=None): - # if self.config.option.verbose: - # line = self._locationline(rep) - # self.write_ensure_prefix(line, "") - # else: - # # ensure that the path is printed before the - # # 1st test of a module starts running - # self.write_fspath_result(self._getfspath(item), "") - def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) + def pytest_runtest_logstart(self, nodeid, location): + # ensure that the path is printed before the + # 1st test of a module starts running + if self.config.option.verbose: + line = self._locationline(*location) + self.write_ensure_prefix(line, "") + else: + self.write_fspath_result(py.path.local(location[0]), "") + def pytest_runtest_logreport(self, report): rep = report res = self.config.hook.pytest_report_teststatus(report=rep) @@ -162,9 +162,10 @@ class TerminalReporter: if not self.config.option.verbose: self.write_fspath_result(rep.fspath, letter) else: - line = self._locationline(rep) + line = self._locationline(*rep.location) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) + #self._tw.write(word, **markup) else: self.ensure_newline() if hasattr(rep, 'node'): @@ -226,21 +227,20 @@ class TerminalReporter: else: excrepr.reprcrash.toterminal(self._tw) - def _locationline(self, rep): + def _locationline(self, fspath, lineno, domain): #collect_fspath = self._getfspath(item) - fspath, lineno, msg = rep.location #if fspath and fspath != collect_fspath: # fspath = "%s <- %s" % ( # self.curdir.bestrelpath(collect_fspath), # self.curdir.bestrelpath(fspath)) if fspath: - fspath = self.curdir.bestrelpath(fspath) + fspath = self.curdir.bestrelpath(py.path.local(fspath)) if lineno is not None: lineno += 1 - if fspath and lineno and msg: - line = "%(fspath)s:%(lineno)s: %(msg)s" - elif fspath and msg: - line = "%(fspath)s: %(msg)s" + if fspath and lineno and domain: + line = "%(fspath)s:%(lineno)s: %(domain)s" + elif fspath and domain: + line = "%(fspath)s: %(domain)s" elif fspath and lineno: line = "%(fspath)s:%(lineno)s %(extrapath)s" else: diff --git a/testing/plugin/test_pytest_python.py b/testing/plugin/test_pytest_python.py index cfc5a054f..4b6d717a0 100644 --- a/testing/plugin/test_pytest_python.py +++ b/testing/plugin/test_pytest_python.py @@ -1109,3 +1109,28 @@ def test_funcarg_lookup_error(testdir): "*1 error*", ]) assert "INTERNAL" not in result.stdout.str() + +class TestReportInfo: + def test_itemreport_reportinfo(self, testdir, linecomp): + testdir.makeconftest(""" + import py + class Function(py.test.collect.Function): + def reportinfo(self): + return "ABCDE", 42, "custom" + """) + item = testdir.getitem("def test_func(): pass") + runner = item.config.pluginmanager.getplugin("runner") + nodeinfo = runner.getitemnodeinfo(item) + assert nodeinfo.location == ("ABCDE", 42, "custom") + + def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + tup = "FGHJ", 42, "custom" + class Plugin: + def pytest_report_iteminfo(self, item): + return tup + item.config.pluginmanager.register(Plugin()) + runner = runner = item.config.pluginmanager.getplugin("runner") + nodeinfo = runner.getitemnodeinfo(item) + location = nodeinfo.location + assert location == tup diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index d6af1450c..feada5178 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -89,47 +89,27 @@ class TestTerminal: assert lines[1].endswith("xy.py .") assert lines[2] == "hello world" - @py.test.mark.xfail(reason="re-implement ItemStart events") - def test_show_path_before_running_test(self, testdir, linecomp): + def test_show_runtest_logstart(self, testdir, linecomp): item = testdir.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) - tr.config.hook.pytest_itemstart(item=item) + nodeid = item.collection.getid(item) + location = item.ihook.pytest_report_iteminfo(item=item) + tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location) linecomp.assert_contains_lines([ - "*test_show_path_before_running_test.py*" + "*test_show_runtest_logstart.py*" ]) - @py.test.mark.xfail(reason="re-implement ItemStart events") - def test_itemreport_reportinfo(self, testdir, linecomp): - testdir.makeconftest(""" - import py - class Function(py.test.collect.Function): - def reportinfo(self): - return "ABCDE", 42, "custom" + def test_runtest_location_shown_before_test_starts(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + import time + time.sleep(20) """) - item = testdir.getitem("def test_func(): pass") - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - tr.config.option.verbose = True - tr.config.hook.pytest_itemstart(item=item) - linecomp.assert_contains_lines([ - "*ABCDE:43: custom*" - ]) - - @py.test.mark.xfail(reason="re-implement ItemStart events") - def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - class Plugin: - def pytest_report_iteminfo(self, item): - return "FGHJ", 42, "custom" - item.config.pluginmanager.register(Plugin()) - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - tr.config.option.verbose = True - tr.config.hook.pytest_itemstart(item=item) - linecomp.assert_contains_lines([ - "*FGHJ:43: custom*" - ]) + child = testdir.spawn_pytest("") + child.expect(".*test_runtest_location.*py") + child.sendeof() + child.kill(15) @py.test.mark.xfail(reason="re-implement subclassing precision reporting") def test_itemreport_subclasses_show_subclassed_file(self, testdir): From 2718fccfa08faaf4e0dfc7a1c5cd434d940b36ce Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 27 Sep 2010 20:48:30 +0200 Subject: [PATCH 29/38] make "tools-on-path" the default and add new random fnmatch-matching method --HG-- branch : trunk --- py/_plugin/pytest_pytester.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/py/_plugin/pytest_pytester.py b/py/_plugin/pytest_pytester.py index 1d2abd2f4..fbf47e872 100644 --- a/py/_plugin/pytest_pytester.py +++ b/py/_plugin/pytest_pytester.py @@ -7,13 +7,14 @@ import sys, os import re import inspect import time +from fnmatch import fnmatch from py._test.config import Config as pytestConfig from py.builtin import print_ def pytest_addoption(parser): group = parser.getgroup("pylib") - group.addoption('--tools-on-path', - action="store_true", dest="toolsonpath", default=False, + group.addoption('--no-tools-on-path', + action="store_true", dest="notoolsonpath", default=False, help=("discover tools on PATH instead of going through py.cmdline.") ) @@ -305,7 +306,7 @@ class TmpTestdir: return self.run(*fullargs) def _getpybinargs(self, scriptname): - if self.request.config.getvalue("toolsonpath"): + if not self.request.config.getvalue("notoolsonpath"): script = py.path.local.sysfind(scriptname) assert script, "script %r not found" % scriptname return (script,) @@ -325,7 +326,7 @@ class TmpTestdir: return self.run(sys.executable, script) def _getsysprepend(self): - if not self.request.config.getvalue("toolsonpath"): + if self.request.config.getvalue("notoolsonpath"): s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) else: s = "" @@ -351,8 +352,8 @@ class TmpTestdir: def spawn_pytest(self, string, expect_timeout=10.0): pexpect = py.test.importorskip("pexpect", "2.4") - if not self.request.config.getvalue("toolsonpath"): - py.test.skip("need --tools-on-path to run py.test script") + if self.request.config.getvalue("notoolsonpath"): + py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") invoke = self._getpybinargs("py.test")[0] cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) @@ -464,13 +465,25 @@ class LineMatcher: def str(self): return "\n".join(self.lines) - def fnmatch_lines(self, lines2): + def _getlines(self, lines2): if isinstance(lines2, str): lines2 = py.code.Source(lines2) if isinstance(lines2, py.code.Source): lines2 = lines2.strip().lines + return lines2 - from fnmatch import fnmatch + def fnmatch_lines_random(self, lines2): + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or fnmatch(x, line): + print_("matched: ", repr(line)) + break + else: + raise ValueError("line %r not found in output" % line) + + def fnmatch_lines(self, lines2): + lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None extralines = [] From f779d3f863f347e38b4b6be9951642a5d75c36d5 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 12:59:48 +0200 Subject: [PATCH 30/38] rework session instantiation and exitstatus handling --HG-- branch : trunk --- py/_plugin/hookspec.py | 4 ++-- py/_plugin/pytest_runner.py | 13 ++++++----- py/_plugin/pytest_terminal.py | 28 +++++++++++++----------- py/_test/session.py | 30 +++++++++++--------------- testing/cmdline/test_cmdline.py | 2 +- testing/plugin/test_pytest_terminal.py | 3 ++- 6 files changed, 41 insertions(+), 39 deletions(-) diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index f9ec47ada..3b75d8925 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -109,7 +109,7 @@ def pytest_runtest_protocol(item): """ implement fixture, run and report about the given test item. """ pytest_runtest_protocol.firstresult = True -def pytest_runtest_logstart(nodeid, location): +def pytest_runtest_logstart(nodeid, location, fspath): """ signal the start of a test run. """ def pytest_runtest_setup(item): @@ -133,7 +133,7 @@ def pytest__teardown_final(session): """ called before test session finishes. """ pytest__teardown_final.firstresult = True -def pytest__teardown_final_logerror(report): +def pytest__teardown_final_logerror(report, session): """ called if runtest_teardown_final failed. """ # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index a9989af2a..7f7672b49 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -27,7 +27,8 @@ def pytest_sessionfinish(session, exitstatus): hook = session.config.hook rep = hook.pytest__teardown_final(session=session) if rep: - hook.pytest__teardown_final_logerror(report=rep) + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 class NodeInfo: def __init__(self, nodeid, nodenames, fspath, location): @@ -52,7 +53,8 @@ def pytest_runtest_protocol(item): nodeinfo = getitemnodeinfo(item) item.ihook.pytest_runtest_logstart( nodeid=nodeinfo.nodeid, - location=nodeinfo.location + location=nodeinfo.location, + fspath=str(item.fspath), ) runtestprotocol(item) return True @@ -80,7 +82,8 @@ def pytest__teardown_final(session): if call.excinfo: ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) call.excinfo.traceback = ntraceback.filter() - return TeardownErrorReport(call.excinfo) + longrepr = call.excinfo.getrepr(funcargs=True) + return TeardownErrorReport(longrepr) def pytest_report_teststatus(report): if report.when in ("setup", "teardown"): @@ -184,8 +187,8 @@ class TestReport(BaseReport): class TeardownErrorReport(BaseReport): outcome = "failed" when = "teardown" - def __init__(self, excinfo): - self.longrepr = excinfo.getrepr(funcargs=True) + def __init__(self, longrepr): + self.longrepr = longrepr def pytest_make_collect_report(collector): result = excinfo = None diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index db463d601..d4553ad5e 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -138,14 +138,14 @@ class TerminalReporter: def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) - def pytest_runtest_logstart(self, nodeid, location): + def pytest_runtest_logstart(self, nodeid, location, fspath): # ensure that the path is printed before the # 1st test of a module starts running if self.config.option.verbose: - line = self._locationline(*location) + line = self._locationline(fspath, *location) self.write_ensure_prefix(line, "") else: - self.write_fspath_result(py.path.local(location[0]), "") + self.write_fspath_result(py.path.local(fspath), "") def pytest_runtest_logreport(self, report): rep = report @@ -158,11 +158,16 @@ class TerminalReporter: if isinstance(word, tuple): word, markup = word else: - markup = {} + if rep.passed: + markup = {'green':True} + elif rep.failed: + markup = {'red':True} + elif rep.skipped: + markup = {'yellow':True} if not self.config.option.verbose: self.write_fspath_result(rep.fspath, letter) else: - line = self._locationline(*rep.location) + line = self._locationline(str(rep.fspath), *rep.location) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) #self._tw.write(word, **markup) @@ -227,13 +232,12 @@ class TerminalReporter: else: excrepr.reprcrash.toterminal(self._tw) - def _locationline(self, fspath, lineno, domain): - #collect_fspath = self._getfspath(item) - #if fspath and fspath != collect_fspath: - # fspath = "%s <- %s" % ( - # self.curdir.bestrelpath(collect_fspath), - # self.curdir.bestrelpath(fspath)) - if fspath: + def _locationline(self, collect_fspath, fspath, lineno, domain): + if fspath and fspath != collect_fspath: + fspath = "%s <- %s" % ( + self.curdir.bestrelpath(py.path.local(collect_fspath)), + self.curdir.bestrelpath(py.path.local(fspath))) + elif fspath: fspath = self.curdir.bestrelpath(py.path.local(fspath)) if lineno is not None: lineno += 1 diff --git a/py/_test/session.py b/py/_test/session.py index 2524972bd..db9bcc1f8 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -44,14 +44,7 @@ class Session(object): self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False - self.collection = Collection(config) - - def sessionfinishes(self, exitstatus): - # XXX move to main loop / refactor mainloop - self.config.hook.pytest_sessionfinish( - session=self, - exitstatus=exitstatus, - ) + self.collection = Collection(config) # XXX move elswehre def pytest_runtest_logreport(self, report): if report.failed: @@ -66,32 +59,33 @@ class Session(object): def main(self): """ main loop for running tests. """ self.shouldstop = False - exitstatus = EXIT_OK + self.exitstatus = EXIT_OK config = self.config try: config.pluginmanager.do_configure(config) config.hook.pytest_sessionstart(session=self) config.hook.pytest_perform_collection(session=self) config.hook.pytest_runtest_mainloop(session=self) - if self._testsfailed: - exitstatus = EXIT_TESTSFAILED - self.sessionfinishes(exitstatus=exitstatus) - config.pluginmanager.do_unconfigure(config) except self.config.Error: raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - exitstatus = EXIT_INTERRUPTED + self.exitstatus = EXIT_INTERRUPTED except: excinfo = py.code.ExceptionInfo() self.config.pluginmanager.notify_exception(excinfo) - exitstatus = EXIT_INTERNALERROR + self.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - if exitstatus in (EXIT_INTERNALERROR, EXIT_INTERRUPTED): - self.sessionfinishes(exitstatus=exitstatus) - return exitstatus + + if not self.exitstatus and self._testsfailed: + self.exitstatus = EXIT_TESTSFAILED + self.config.hook.pytest_sessionfinish( + session=self, exitstatus=self.exitstatus, + ) + config.pluginmanager.do_unconfigure(config) + return self.exitstatus class Collection: def __init__(self, config): diff --git a/testing/cmdline/test_cmdline.py b/testing/cmdline/test_cmdline.py index cc3ce1399..8438d876b 100644 --- a/testing/cmdline/test_cmdline.py +++ b/testing/cmdline/test_cmdline.py @@ -7,7 +7,7 @@ def test_cmdmain(name, pytestconfig): main = getattr(py.cmdline, name) assert py.builtin.callable(main) assert name[:2] == "py" - if pytestconfig.getvalue("toolsonpath"): + if not pytestconfig.getvalue("notoolsonpath"): scriptname = "py." + name[2:] assert py.path.local.sysfind(scriptname), scriptname diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index feada5178..c84ba4cb1 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -95,7 +95,8 @@ class TestTerminal: item.config.pluginmanager.register(tr) nodeid = item.collection.getid(item) location = item.ihook.pytest_report_iteminfo(item=item) - tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location) + tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, + location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ "*test_show_runtest_logstart.py*" ]) From a60e470573a3c8330d3eb91c357aa11c6574a933 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 15:24:36 +0200 Subject: [PATCH 31/38] fix a collection bug where a::b::c could not be resolved properly if there are multiple 'b' nodes. --HG-- branch : trunk --- ISSUES.txt | 2 ++ py/_test/session.py | 67 +++++++++++++++++++++----------------- testing/acceptance_test.py | 20 ++++++++++++ 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/ISSUES.txt b/ISSUES.txt index 4474b4a29..415ed8b9a 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -4,6 +4,8 @@ tags: bug 1.4 core xdist * reportinfo -> location in hooks and items * check oejskit plugin compatibility +* terminal reporting - dot-printing +* some simple profiling refine session initialization / fix custom collect crash --------------------------------------------------------------- diff --git a/py/_test/session.py b/py/_test/session.py index db9bcc1f8..4d40908d0 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -138,15 +138,14 @@ class Collection: def getbyid(self, id): """ return one or more nodes matching the id. """ - matching = [self._topcollector] - if not id: - return matching - names = id.split("::") + names = filter(None, id.split("::")) + if names and '/' in names[0]: + names[:1] = names[0].split("/") + return self._match([self._topcollector], names) + + def _match(self, matching, names): while names: name = names.pop(0) - newnames = name.split("/") - name = newnames[0] - names[:0] = newnames[1:] l = [] for current in matching: for x in current._memocollect(): @@ -169,22 +168,28 @@ class Collection: return nodes def perform_collect(self): - idlist = [self._parsearg(arg) for arg in self.config.args] nodes = [] - for names in idlist: - self.genitems([self._topcollector], names, nodes) + for arg in self.config.args: + names = self._parsearg(arg) + try: + self.genitems([self._topcollector], names, nodes) + except NoMatch: + raise self.config.Error("can't collect: %s" % (arg,)) return nodes def genitems(self, matching, names, result): if not matching: assert not names - return result - names = list(names) - name = names and names.pop(0) or None + return + if names: + name = names[0] + names = names[1:] + else: + name = None for node in matching: if isinstance(node, py.test.collect.Item): if name is None: - self.config.hook.pytest_log_itemcollect(item=node) + node.ihook.pytest_log_itemcollect(item=node) result.append(node) continue assert isinstance(node, py.test.collect.Collector) @@ -192,27 +197,31 @@ class Collection: rep = node.ihook.pytest_make_collect_report(collector=node) #print "matching", rep.result, "against name", name if rep.passed: - if name: - matched = False - for subcol in rep.result: - if subcol.name != name and subcol.name == "()": - names.insert(0, name) - name = "()" - # see doctests/custom naming XXX - if subcol.name == name or subcol.fspath.basename == name: - self.genitems([subcol], names, result) - matched = True - if not matched: - raise self.config.Error( - "can't collect: %s" % (name,)) - - else: + if not name: self.genitems(rep.result, [], result) + else: + matched = False + for x in rep.result: + try: + if x.name == name or x.fspath.basename == name: + self.genitems([x], names, result) + matched = True + elif x.name == "()": # XXX special Instance() case + self.genitems([x], [name] + names, result) + matched = True + except NoMatch: + pass + if not matched: + node.ihook.pytest_collectreport(report=rep) + raise NoMatch(name) node.ihook.pytest_collectreport(report=rep) x = getattr(self, 'shouldstop', None) if x: raise Session.Interrupted(x) +class NoMatch(Exception): + """ raised if genitems cannot locate a matching names. """ + def gettopdir(args): """ return the top directory for the given paths. if the common base dir resides in a python package diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index faf27ea12..0b521eef8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -191,3 +191,23 @@ class TestGeneralUsage: for dirname, other_dirname in [('a', 'b'), ('b', 'a')]: result = testdir.runpytest(dirname) assert result.ret == 0, "test_sibling_conftest: py.test run for '%s', but '%s/conftest.py' loaded." % (dirname, other_dirname) + + def test_multiple_items_per_collector_byid(self, testdir): + c = testdir.makeconftest(""" + import py + class MyItem(py.test.collect.Item): + def runtest(self): + pass + class MyCollector(py.test.collect.File): + def collect(self): + return [MyItem(name="xyz", parent=self)] + def pytest_collect_file(path, parent): + if path.basename.startswith("conftest"): + return MyCollector(path, parent) + """) + result = testdir.runpytest(c.basename+"::"+"xyz") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*1 pass*", + ]) + From e2e01a85854a93b7d9adea997437a8ff04db2ea1 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 15:53:10 +0200 Subject: [PATCH 32/38] refine reporting a bit, show only "dots" for distributed testing --HG-- branch : trunk --- py/_plugin/pytest_terminal.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/py/_plugin/pytest_terminal.py b/py/_plugin/pytest_terminal.py index d4553ad5e..78e49ac14 100644 --- a/py/_plugin/pytest_terminal.py +++ b/py/_plugin/pytest_terminal.py @@ -155,18 +155,21 @@ class TerminalReporter: if not letter and not word: # probably passed setup/teardown return - if isinstance(word, tuple): - word, markup = word - else: - if rep.passed: - markup = {'green':True} - elif rep.failed: - markup = {'red':True} - elif rep.skipped: - markup = {'yellow':True} if not self.config.option.verbose: - self.write_fspath_result(rep.fspath, letter) + if not hasattr(rep, 'node'): + self.write_fspath_result(rep.fspath, letter) + else: + self._tw.write(letter) else: + if isinstance(word, tuple): + word, markup = word + else: + if rep.passed: + markup = {'green':True} + elif rep.failed: + markup = {'red':True} + elif rep.skipped: + markup = {'yellow':True} line = self._locationline(str(rep.fspath), *rep.location) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) From 88915aa57dd5a8a0b021de6a12fa4d9d4c6eef7e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 15:58:23 +0200 Subject: [PATCH 33/38] fix tox.ini invocation --HG-- branch : trunk --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index e791b84c8..158e1c32f 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ sdistsrc={distshare}/py-* [testenv] changedir=testing commands= - py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml --tools-on-path [] + py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= pexpect [testenv:py27] @@ -21,7 +21,7 @@ deps= {distshare}/pytest-xdist-* commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml --tools-on-path [] + --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py26] basepython=python2.6 From 81ec29a597c345eb8085a21b6763971f6268dca7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 16:38:46 +0200 Subject: [PATCH 34/38] fix python3 bugs --HG-- branch : trunk --- py/_test/session.py | 2 +- testing/test_deprecated_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py/_test/session.py b/py/_test/session.py index 4d40908d0..cd6320eb1 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -138,7 +138,7 @@ class Collection: def getbyid(self, id): """ return one or more nodes matching the id. """ - names = filter(None, id.split("::")) + names = [x for x in id.split("::") if x] if names and '/' in names[0]: names[:1] = names[0].split("/") return self._match([self._topcollector], names) diff --git a/testing/test_deprecated_api.py b/testing/test_deprecated_api.py index 67a1c6700..fb3d738d5 100644 --- a/testing/test_deprecated_api.py +++ b/testing/test_deprecated_api.py @@ -170,10 +170,10 @@ class TestCollectDeprecated: if path.basename == "testme.xxx": return Module(path, parent=self) return super(Directory, self).consider_file(path) + """) #def pytest_collect_file(path, parent): # if path.basename == "testme.xxx": # return Module(path, parent=parent) - """) testme = testdir.makefile('xxx', testme="hello") config = testdir.parseconfig(testme) col = testdir.getnode(config, testme) From e2c11f1ddb1e64c9a154b59c0af5b3afe7833e01 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 Sep 2010 17:37:20 +0200 Subject: [PATCH 35/38] fix python3 issues, add py32 environment --HG-- branch : trunk --- testing/code/test_source.py | 2 +- testing/plugin/test_pytest_terminal.py | 1 - tox.ini | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 4be583803..afcd28d7a 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -348,7 +348,7 @@ def test_deindent(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] -@py.test.mark.xfail("sys.version_info[:2] != (2,7)") +@py.test.mark.xfail("sys.version_info[:2] != (2,7) and sys.version_info[:2]<(3,2)") def test_source_of_class_at_eof_without_newline(tmpdir): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. diff --git a/testing/plugin/test_pytest_terminal.py b/testing/plugin/test_pytest_terminal.py index c84ba4cb1..c54e5729d 100644 --- a/testing/plugin/test_pytest_terminal.py +++ b/testing/plugin/test_pytest_terminal.py @@ -112,7 +112,6 @@ class TestTerminal: child.sendeof() child.kill(15) - @py.test.mark.xfail(reason="re-implement subclassing precision reporting") def test_itemreport_subclasses_show_subclassed_file(self, testdir): p1 = testdir.makepyfile(test_p1=""" class BaseTests: diff --git a/tox.ini b/tox.ini index 158e1c32f..770e998d9 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,10 @@ basepython=python2.4 [testenv:py31] basepython=python3.1 deps= +[testenv:py32] +basepython=python3.2 +deps= +#{distshare}/pytest-xdist-* #[testenv:pypy] #python=pypy-c [testenv:jython] From cd5676adc4db7c19dc08b0d905ce48c140b8574d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 30 Sep 2010 23:15:41 +0100 Subject: [PATCH 36/38] Truncate the text passed to difflib where possible This stops difflib from printing many lines which had no change in them anyway. It also avoids a bug in difflib which fails or hangs when there are many trailing lines which are all identical. --HG-- branch : trunk --- doc/example/assertion/failure_demo.py | 18 ++++++++++ py/_plugin/pytest_assertion.py | 52 +++++++++++++++++++-------- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/doc/example/assertion/failure_demo.py b/doc/example/assertion/failure_demo.py index b99253ee8..f0266b82c 100644 --- a/doc/example/assertion/failure_demo.py +++ b/doc/example/assertion/failure_demo.py @@ -128,15 +128,33 @@ class TestSpecialisedExplanations(object): def test_eq_multiline_text(self): assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + def test_eq_long_text(self): + a = '1'*100 + 'a' + '2'*100 + b = '1'*100 + 'b' + '2'*100 + assert a == b + + def test_eq_long_text_multiline(self): + a = '1\n'*100 + 'a' + '2\n'*100 + b = '1\n'*100 + 'b' + '2\n'*100 + assert a == b + def test_eq_list(self): assert [0, 1, 2] == [0, 1, 3] + def test_eq_list_long(self): + a = [0]*100 + [1] + [3]*100 + b = [0]*100 + [2] + [3]*100 + assert a == b + def test_eq_dict(self): assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} def test_eq_set(self): assert set([0, 10, 11, 12]) == set([0, 20, 21]) + def test_in_list(self): + assert 1 in [0, 2, 3, 4, 5] + def globf(x): return x+1 diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index 9e6355f41..b58b91985 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -54,18 +54,16 @@ def pytest_assert_binrepr(op, left, right): explanation = None if op == '==': if istext(left) and istext(right): - explanation = [line.strip('\n') for line in - py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] + explanation = _diff_text(left, right) elif issequence(left) and issequence(right): explanation = _compare_eq_sequence(left, right) elif isset(left) and isset(right): explanation = _compare_eq_set(left, right) elif isdict(left) and isdict(right): - explanation = _pprint_diff(left, right) + explanation = _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) elif op == 'in': - # XXX - pass + pass # XXX if not explanation: return None @@ -77,6 +75,38 @@ def pytest_assert_binrepr(op, left, right): return [summary] + explanation +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + def _compare_eq_sequence(left, right): explanation = [] for i in range(min(len(left), len(right))): @@ -90,14 +120,8 @@ def _compare_eq_sequence(left, right): elif len(left) < len(right): explanation += ['Right contains more items, ' 'first extra item: %s' % right[len(left)]] - return explanation + _pprint_diff(left, right) - - -def _pprint_diff(left, right): - """Make explanation using pprint and difflib""" - return [line.strip('\n') for line in - py.std.difflib.ndiff(py.std.pprint.pformat(left).splitlines(), - py.std.pprint.pformat(right).splitlines())] + return explanation + _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) def _compare_eq_set(left, right): From 1ff173baee58f8b9d6a53c23e0c712fab2126ee9 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 2 Oct 2010 18:47:39 +0200 Subject: [PATCH 37/38] refactor assert interpretation to invoke a simple callable and let the assertion plugin handle the hook invocation and its multi-results and also pass in an (optional) test config object to the hook. Add and refactor also a few tests. --HG-- branch : trunk --- py/__init__.py | 1 + py/_code/_assertionnew.py | 22 +--- py/_code/assertion.py | 2 +- py/_plugin/hookspec.py | 2 +- py/_plugin/pytest_assertion.py | 11 +- testing/code/test_assertion.py | 10 ++ testing/plugin/test_pytest_assertion.py | 165 +++++++++++++----------- 7 files changed, 123 insertions(+), 90 deletions(-) diff --git a/py/__init__.py b/py/__init__.py index e62e1a72a..814161cea 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -93,6 +93,7 @@ py.apipkg.initpkg(__name__, dict( '_AssertionError' : '._code.assertion:AssertionError', '_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret' : '._code.assertion:reinterpret', + '_binrepr' : '._code.assertion:_binrepr', }, # backports and additions of builtins diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 2c40a93dc..5fdc1088b 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -108,16 +108,10 @@ unary_map = { class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information. - - The _pytesthook attribute is used to detect if the py.test - pytest_assertion plugin is loaded and if so call it's hooks. - """ + """Interpret AST nodes to gleam useful debugging information. """ def __init__(self, frame): self.frame = frame - self._pytesthook = getattr(py.builtin.builtins.AssertionError, - "_pytesthook") def generic_visit(self, node): # Fallback when we don't have a special implementation. @@ -183,14 +177,12 @@ class DebugInterpreter(ast.NodeVisitor): if not result: break left_explanation, left_result = next_explanation, next_result - if self._pytesthook: - hook_result = self._pytesthook.pytest_assert_binrepr( - op=op_symbol, left=left_result, right=next_result) - if hook_result: - for new_expl in hook_result: - if new_expl: - explanation = '\n~'.join(new_expl) - break + + binrepr = py.code._binrepr + if binrepr: + res = binrepr(op_symbol, left_result, next_result) + if res: + explanation = res return explanation, result def visit_BoolOp(self, boolop): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index adbbce7c5..675643e57 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -3,6 +3,7 @@ import py BuiltinAssertionError = py.builtin.builtins.AssertionError +_binrepr = None # if set, will be called by assert reinterp for comparison ops def _format_explanation(explanation): """This formats an explanation @@ -49,7 +50,6 @@ def _format_explanation(explanation): class AssertionError(BuiltinAssertionError): - def __init__(self, *args): BuiltinAssertionError.__init__(self, *args) if args: diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 315da074a..2281d149c 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -151,7 +151,7 @@ def pytest_sessionfinish(session, exitstatus): # hooks for customising the assert methods # ------------------------------------------------------------------------- -def pytest_assert_binrepr(op, left, right): +def pytest_assert_binrepr(config, op, left, right): """Customise explanation for binary operators Return None or an empty list for no custom explanation, otherwise diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index b58b91985..bdfd0e819 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -15,13 +15,22 @@ def pytest_configure(config): if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() config._oldassertion = py.builtin.builtins.AssertionError + config._oldbinrepr = py.code._binrepr py.builtin.builtins.AssertionError = py.code._AssertionError - py.builtin.builtins.AssertionError._pytesthook = config.hook + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assert_binrepr( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + py.code._binrepr = callbinrepr def pytest_unconfigure(config): if hasattr(config, '_oldassertion'): py.builtin.builtins.AssertionError = config._oldassertion + py.code._binrepr = config._oldbinrepr del config._oldassertion + del config._oldbinrepr def warn_about_missing_assertion(): try: diff --git a/testing/code/test_assertion.py b/testing/code/test_assertion.py index d417b0f75..07e9fa754 100644 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -217,3 +217,13 @@ def test_underscore_api(): py.code._AssertionError py.code._reinterpret_old # used by pypy py.code._reinterpret + +@py.test.mark.skipif("sys.version_info < (2,6)") +def test_assert_customizable_binrepr(monkeypatch): + monkeypatch.setattr(py.code, '_binrepr', lambda *args: 'hello') + try: + assert 3 == 4 + except AssertionError: + e = exvalue() + s = str(e) + assert "hello" in s diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 0dd64f473..8740b733a 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -3,29 +3,104 @@ import sys import py import py._plugin.pytest_assertion as plugin +needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") -def getframe(): - """Return the frame of the caller as a py.code.Frame object""" - return py.code.Frame(sys._getframe(1)) +def interpret(expr): + return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) -def interpret(expr, frame): - anew = py.test.importorskip('py._code._assertionnew') - return anew.interpret(expr, frame) +class TestBinReprIntegration: + pytestmark = needsnewassert -def pytest_funcarg__hook(request): - class MockHook(object): - def __init__(self): - self.called = False - self.args = tuple() - self.kwargs = dict() + def pytest_funcarg__hook(self, request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() - def __call__(self, op, left, right): - self.called = True - self.op = op - self.left = left - self.right = right - return MockHook() + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + mockhook = MockHook() + monkeypatch = request.getfuncargvalue("monkeypatch") + monkeypatch.setattr(py.code, '_binrepr', mockhook) + return mockhook + def test_pytest_assert_binrepr_called(self, hook): + interpret('assert 0 == 1') + assert hook.called + + + def test_pytest_assert_binrepr_args(self, hook): + interpret('assert [0, 1] == [0, 2]') + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] + + def test_configure_unconfigure(self, testdir, hook): + assert hook == py.code._binrepr + config = testdir.parseconfig() + plugin.pytest_configure(config) + assert hook != py.code._binrepr + plugin.pytest_unconfigure(config) + assert hook == py.code._binrepr + +class TestAssert_binrepr: + def test_different_types(self): + assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None + + def test_summary(self): + summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] + assert len(summary) < 65 + + def test_text_diff(self): + diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] + assert '- spam' in diff + assert '+ eggs' in diff + + def test_multiline_text_diff(self): + left = 'foo\nspam\nbar' + right = 'foo\neggs\nbar' + diff = plugin.pytest_assert_binrepr('==', left, right) + assert '- spam' in diff + assert '+ eggs' in diff + + def test_list(self): + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) + assert len(expl) > 1 + + def test_list_different_lenghts(self): + expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) + assert len(expl) > 1 + expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) + assert len(expl) > 1 + + def test_dict(self): + expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) + assert len(expl) > 1 + + def test_set(self): + expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) + assert len(expl) > 1 + +@needsnewassert +def test_pytest_assert_binrepr_integration(testdir): + testdir.makepyfile(""" + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + ]) def test_functional(testdir): testdir.makepyfile(""" @@ -78,57 +153,3 @@ def test_traceback_failure(testdir): "*test_traceback_failure.py:4: AssertionError" ]) - -def test_pytest_assert_binrepr_called(monkeypatch, hook): - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_binrepr', hook) - interpret('assert 0 == 1', getframe()) - assert hook.called - - -def test_pytest_assert_binrepr_args(monkeypatch, hook): - monkeypatch.setattr(py._plugin.pytest_assertion, - 'pytest_assert_binrepr', hook) - interpret('assert [0, 1] == [0, 2]', getframe()) - assert hook.op == '==' - assert hook.left == [0, 1] - assert hook.right == [0, 2] - - -class TestAssertCompare: - def test_different_types(self): - assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None - - def test_summary(self): - summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] - assert len(summary) < 65 - - def test_text_diff(self): - diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] - assert '- spam' in diff - assert '+ eggs' in diff - - def test_multiline_text_diff(self): - left = 'foo\nspam\nbar' - right = 'foo\neggs\nbar' - diff = plugin.pytest_assert_binrepr('==', left, right) - assert '- spam' in diff - assert '+ eggs' in diff - - def test_list(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) - assert len(expl) > 1 - - def test_list_different_lenghts(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) - assert len(expl) > 1 - expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) - assert len(expl) > 1 - - def test_dict(self): - expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) - assert len(expl) > 1 - - def test_set(self): - expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) - assert len(expl) > 1 From 77cacb99eeaac2fea0ac4a3c63633345f68c4051 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 2 Oct 2010 19:00:47 +0200 Subject: [PATCH 38/38] to better match the naming of the corresponding AST (and in case we want to add more customizations later) rename pytest_assert_binrepr -> pytest_assertrepr_compare rename binrepr -> reprcompare --HG-- branch : trunk --- py/__init__.py | 2 +- py/_code/_assertionnew.py | 6 ++--- py/_code/assertion.py | 2 +- py/_plugin/hookspec.py | 13 +++++----- py/_plugin/pytest_assertion.py | 12 ++++----- testing/code/test_assertion.py | 4 +-- testing/plugin/test_pytest_assertion.py | 34 ++++++++++++------------- 7 files changed, 36 insertions(+), 37 deletions(-) diff --git a/py/__init__.py b/py/__init__.py index 814161cea..4a55d189d 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -93,7 +93,7 @@ py.apipkg.initpkg(__name__, dict( '_AssertionError' : '._code.assertion:AssertionError', '_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret' : '._code.assertion:reinterpret', - '_binrepr' : '._code.assertion:_binrepr', + '_reprcompare' : '._code.assertion:_reprcompare', }, # backports and additions of builtins diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 5fdc1088b..c3d8df6aa 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -178,9 +178,9 @@ class DebugInterpreter(ast.NodeVisitor): break left_explanation, left_result = next_explanation, next_result - binrepr = py.code._binrepr - if binrepr: - res = binrepr(op_symbol, left_result, next_result) + rcomp = py.code._reprcompare + if rcomp: + res = rcomp(op_symbol, left_result, next_result) if res: explanation = res return explanation, result diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 675643e57..e77f250df 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -3,7 +3,7 @@ import py BuiltinAssertionError = py.builtin.builtins.AssertionError -_binrepr = None # if set, will be called by assert reinterp for comparison ops +_reprcompare = None # if set, will be called by assert reinterp for comparison ops def _format_explanation(explanation): """This formats an explanation diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 2281d149c..02a310da5 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -151,14 +151,13 @@ def pytest_sessionfinish(session, exitstatus): # hooks for customising the assert methods # ------------------------------------------------------------------------- -def pytest_assert_binrepr(config, op, left, right): - """Customise explanation for binary operators +def pytest_assertrepr_compare(config, op, left, right): + """return explanation for comparisons in failing assert expressions. - Return None or an empty list for no custom explanation, otherwise - return a list of strings. The strings will be joined by newlines - but any newlines *in* a string will be escaped. Note that all but - the first line will be indented sligthly, the intention is for the - first line to be a summary. + Return None for no custom explanation, otherwise return a list + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will + be indented sligthly, the intention is for the first line to be a summary. """ # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index bdfd0e819..e9a04975f 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -15,20 +15,20 @@ def pytest_configure(config): if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() config._oldassertion = py.builtin.builtins.AssertionError - config._oldbinrepr = py.code._binrepr + config._oldbinrepr = py.code._reprcompare py.builtin.builtins.AssertionError = py.code._AssertionError def callbinrepr(op, left, right): - hook_result = config.hook.pytest_assert_binrepr( + hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) for new_expl in hook_result: if new_expl: return '\n~'.join(new_expl) - py.code._binrepr = callbinrepr + py.code._reprcompare = callbinrepr def pytest_unconfigure(config): if hasattr(config, '_oldassertion'): py.builtin.builtins.AssertionError = config._oldassertion - py.code._binrepr = config._oldbinrepr + py.code._reprcompare = config._oldbinrepr del config._oldassertion del config._oldbinrepr @@ -49,8 +49,8 @@ except NameError: basestring = str -def pytest_assert_binrepr(op, left, right): - """Make specialised explanations for some operators/operands""" +def pytest_assertrepr_compare(op, left, right): + """return specialised explanations for some operators/operands""" left_repr = py.io.saferepr(left, maxsize=30) right_repr = py.io.saferepr(right, maxsize=30) summary = '%s %s %s' % (left_repr, op, right_repr) diff --git a/testing/code/test_assertion.py b/testing/code/test_assertion.py index 07e9fa754..4904f7fb1 100644 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -219,8 +219,8 @@ def test_underscore_api(): py.code._reinterpret @py.test.mark.skipif("sys.version_info < (2,6)") -def test_assert_customizable_binrepr(monkeypatch): - monkeypatch.setattr(py.code, '_binrepr', lambda *args: 'hello') +def test_assert_customizable_reprcompare(monkeypatch): + monkeypatch.setattr(py.code, '_reprcompare', lambda *args: 'hello') try: assert 3 == 4 except AssertionError: diff --git a/testing/plugin/test_pytest_assertion.py b/testing/plugin/test_pytest_assertion.py index 8740b733a..1b98c8ae8 100644 --- a/testing/plugin/test_pytest_assertion.py +++ b/testing/plugin/test_pytest_assertion.py @@ -25,68 +25,68 @@ class TestBinReprIntegration: self.right = right mockhook = MockHook() monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(py.code, '_binrepr', mockhook) + monkeypatch.setattr(py.code, '_reprcompare', mockhook) return mockhook - def test_pytest_assert_binrepr_called(self, hook): + def test_pytest_assertrepr_compare_called(self, hook): interpret('assert 0 == 1') assert hook.called - def test_pytest_assert_binrepr_args(self, hook): + def test_pytest_assertrepr_compare_args(self, hook): interpret('assert [0, 1] == [0, 2]') assert hook.op == '==' assert hook.left == [0, 1] assert hook.right == [0, 2] def test_configure_unconfigure(self, testdir, hook): - assert hook == py.code._binrepr + assert hook == py.code._reprcompare config = testdir.parseconfig() plugin.pytest_configure(config) - assert hook != py.code._binrepr + assert hook != py.code._reprcompare plugin.pytest_unconfigure(config) - assert hook == py.code._binrepr + assert hook == py.code._reprcompare -class TestAssert_binrepr: +class TestAssert_reprcompare: def test_different_types(self): - assert plugin.pytest_assert_binrepr('==', [0, 1], 'foo') is None + assert plugin.pytest_assertrepr_compare('==', [0, 1], 'foo') is None def test_summary(self): - summary = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2])[0] + summary = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 2])[0] assert len(summary) < 65 def test_text_diff(self): - diff = plugin.pytest_assert_binrepr('==', 'spam', 'eggs')[1:] + diff = plugin.pytest_assertrepr_compare('==', 'spam', 'eggs')[1:] assert '- spam' in diff assert '+ eggs' in diff def test_multiline_text_diff(self): left = 'foo\nspam\nbar' right = 'foo\neggs\nbar' - diff = plugin.pytest_assert_binrepr('==', left, right) + diff = plugin.pytest_assertrepr_compare('==', left, right) assert '- spam' in diff assert '+ eggs' in diff def test_list(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 2]) + expl = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 2]) assert len(expl) > 1 def test_list_different_lenghts(self): - expl = plugin.pytest_assert_binrepr('==', [0, 1], [0, 1, 2]) + expl = plugin.pytest_assertrepr_compare('==', [0, 1], [0, 1, 2]) assert len(expl) > 1 - expl = plugin.pytest_assert_binrepr('==', [0, 1, 2], [0, 1]) + expl = plugin.pytest_assertrepr_compare('==', [0, 1, 2], [0, 1]) assert len(expl) > 1 def test_dict(self): - expl = plugin.pytest_assert_binrepr('==', {'a': 0}, {'a': 1}) + expl = plugin.pytest_assertrepr_compare('==', {'a': 0}, {'a': 1}) assert len(expl) > 1 def test_set(self): - expl = plugin.pytest_assert_binrepr('==', set([0, 1]), set([0, 2])) + expl = plugin.pytest_assertrepr_compare('==', set([0, 1]), set([0, 2])) assert len(expl) > 1 @needsnewassert -def test_pytest_assert_binrepr_integration(testdir): +def test_pytest_assertrepr_compare_integration(testdir): testdir.makepyfile(""" def test_hello(): x = set(range(100))