Initial patch as sent to py-dev
With a small but disasterous typo fixed though. --HG-- branch : trunk
This commit is contained in:
parent
95bafbccd1
commit
cd013746cf
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
|
@ -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())]
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue