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
This commit is contained in:
holger krekel 2010-10-02 18:47:39 +02:00
parent b56d3c223d
commit 1ff173baee
7 changed files with 123 additions and 90 deletions

View File

@ -93,6 +93,7 @@ py.apipkg.initpkg(__name__, dict(
'_AssertionError' : '._code.assertion:AssertionError', '_AssertionError' : '._code.assertion:AssertionError',
'_reinterpret_old' : '._code.assertion:reinterpret_old', '_reinterpret_old' : '._code.assertion:reinterpret_old',
'_reinterpret' : '._code.assertion:reinterpret', '_reinterpret' : '._code.assertion:reinterpret',
'_binrepr' : '._code.assertion:_binrepr',
}, },
# backports and additions of builtins # backports and additions of builtins

View File

@ -108,16 +108,10 @@ unary_map = {
class DebugInterpreter(ast.NodeVisitor): 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): def __init__(self, frame):
self.frame = frame self.frame = frame
self._pytesthook = getattr(py.builtin.builtins.AssertionError,
"_pytesthook")
def generic_visit(self, node): def generic_visit(self, node):
# Fallback when we don't have a special implementation. # Fallback when we don't have a special implementation.
@ -183,14 +177,12 @@ class DebugInterpreter(ast.NodeVisitor):
if not result: if not result:
break break
left_explanation, left_result = next_explanation, next_result left_explanation, left_result = next_explanation, next_result
if self._pytesthook:
hook_result = self._pytesthook.pytest_assert_binrepr( binrepr = py.code._binrepr
op=op_symbol, left=left_result, right=next_result) if binrepr:
if hook_result: res = binrepr(op_symbol, left_result, next_result)
for new_expl in hook_result: if res:
if new_expl: explanation = res
explanation = '\n~'.join(new_expl)
break
return explanation, result return explanation, result
def visit_BoolOp(self, boolop): def visit_BoolOp(self, boolop):

View File

@ -3,6 +3,7 @@ import py
BuiltinAssertionError = py.builtin.builtins.AssertionError BuiltinAssertionError = py.builtin.builtins.AssertionError
_binrepr = None # if set, will be called by assert reinterp for comparison ops
def _format_explanation(explanation): def _format_explanation(explanation):
"""This formats an explanation """This formats an explanation
@ -49,7 +50,6 @@ def _format_explanation(explanation):
class AssertionError(BuiltinAssertionError): class AssertionError(BuiltinAssertionError):
def __init__(self, *args): def __init__(self, *args):
BuiltinAssertionError.__init__(self, *args) BuiltinAssertionError.__init__(self, *args)
if args: if args:

View File

@ -151,7 +151,7 @@ def pytest_sessionfinish(session, exitstatus):
# hooks for customising the assert methods # 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 """Customise explanation for binary operators
Return None or an empty list for no custom explanation, otherwise Return None or an empty list for no custom explanation, otherwise

View File

@ -15,13 +15,22 @@ def pytest_configure(config):
if not config.getvalue("noassert") and not config.getvalue("nomagic"): if not config.getvalue("noassert") and not config.getvalue("nomagic"):
warn_about_missing_assertion() warn_about_missing_assertion()
config._oldassertion = py.builtin.builtins.AssertionError config._oldassertion = py.builtin.builtins.AssertionError
config._oldbinrepr = py.code._binrepr
py.builtin.builtins.AssertionError = py.code._AssertionError 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): def pytest_unconfigure(config):
if hasattr(config, '_oldassertion'): if hasattr(config, '_oldassertion'):
py.builtin.builtins.AssertionError = config._oldassertion py.builtin.builtins.AssertionError = config._oldassertion
py.code._binrepr = config._oldbinrepr
del config._oldassertion del config._oldassertion
del config._oldbinrepr
def warn_about_missing_assertion(): def warn_about_missing_assertion():
try: try:

View File

@ -217,3 +217,13 @@ def test_underscore_api():
py.code._AssertionError py.code._AssertionError
py.code._reinterpret_old # used by pypy py.code._reinterpret_old # used by pypy
py.code._reinterpret 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

View File

@ -3,29 +3,104 @@ import sys
import py import py
import py._plugin.pytest_assertion as plugin import py._plugin.pytest_assertion as plugin
needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)")
def getframe(): def interpret(expr):
"""Return the frame of the caller as a py.code.Frame object""" return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1)))
return py.code.Frame(sys._getframe(1))
def interpret(expr, frame): class TestBinReprIntegration:
anew = py.test.importorskip('py._code._assertionnew') pytestmark = needsnewassert
return anew.interpret(expr, frame)
def pytest_funcarg__hook(request): def pytest_funcarg__hook(self, request):
class MockHook(object): class MockHook(object):
def __init__(self): def __init__(self):
self.called = False self.called = False
self.args = tuple() self.args = tuple()
self.kwargs = dict() self.kwargs = dict()
def __call__(self, op, left, right): def __call__(self, op, left, right):
self.called = True self.called = True
self.op = op self.op = op
self.left = left self.left = left
self.right = right self.right = right
return MockHook() 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): def test_functional(testdir):
testdir.makepyfile(""" testdir.makepyfile("""
@ -78,57 +153,3 @@ def test_traceback_failure(testdir):
"*test_traceback_failure.py:4: AssertionError" "*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