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:
parent
b56d3c223d
commit
1ff173baee
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in New Issue