Initial patch as sent to py-dev

With a small but disasterous typo fixed though.

--HG--
branch : trunk
This commit is contained in:
Floris Bruynooghe 2010-09-06 19:35:17 +01:00
parent 95bafbccd1
commit cd013746cf
5 changed files with 160 additions and 9 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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)
# -------------------------------------------------------------------------

View File

@ -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())]

View File

@ -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