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):
|
def visit_Compare(self, comp):
|
||||||
left = comp.left
|
left = comp.left
|
||||||
left_explanation, left_result = self.visit(left)
|
left_explanation, left_result = self.visit(left)
|
||||||
got_result = False
|
|
||||||
for op, next_op in zip(comp.ops, comp.comparators):
|
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)
|
next_explanation, next_result = self.visit(next_op)
|
||||||
op_symbol = operator_map[op.__class__]
|
op_symbol = operator_map[op.__class__]
|
||||||
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
||||||
|
@ -177,9 +174,16 @@ class DebugInterpreter(ast.NodeVisitor):
|
||||||
__exprinfo_right=next_result)
|
__exprinfo_right=next_result)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Failure(explanation)
|
raise Failure(explanation)
|
||||||
else:
|
if not result:
|
||||||
got_result = True
|
break
|
||||||
left_explanation, left_result = next_explanation, next_result
|
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
|
return explanation, result
|
||||||
|
|
||||||
def visit_BoolOp(self, boolop):
|
def visit_BoolOp(self, boolop):
|
||||||
|
|
|
@ -5,12 +5,20 @@ BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||||
|
|
||||||
|
|
||||||
def _format_explanation(explanation):
|
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')
|
raw_lines = (explanation or '').split('\n')
|
||||||
# escape newlines not followed by { and }
|
# escape newlines not followed by {, } and ~
|
||||||
lines = [raw_lines[0]]
|
lines = [raw_lines[0]]
|
||||||
for l in raw_lines[1:]:
|
for l in raw_lines[1:]:
|
||||||
if l.startswith('{') or l.startswith('}'):
|
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
|
||||||
lines.append(l)
|
lines.append(l)
|
||||||
else:
|
else:
|
||||||
lines[-1] += '\\n' + l
|
lines[-1] += '\\n' + l
|
||||||
|
@ -28,11 +36,14 @@ def _format_explanation(explanation):
|
||||||
stackcnt[-1] += 1
|
stackcnt[-1] += 1
|
||||||
stackcnt.append(0)
|
stackcnt.append(0)
|
||||||
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
||||||
else:
|
elif line.startswith('}'):
|
||||||
assert line.startswith('}')
|
assert line.startswith('}')
|
||||||
stack.pop()
|
stack.pop()
|
||||||
stackcnt.pop()
|
stackcnt.pop()
|
||||||
result[stack[-1]] += line[1:]
|
result[stack[-1]] += line[1:]
|
||||||
|
else:
|
||||||
|
assert line.startswith('~')
|
||||||
|
result.append(' '*len(stack) + line[1:])
|
||||||
assert len(stack) == 1
|
assert len(stack) == 1
|
||||||
return '\n'.join(result)
|
return '\n'.join(result)
|
||||||
|
|
||||||
|
|
|
@ -123,6 +123,19 @@ def pytest_sessionstart(session):
|
||||||
def pytest_sessionfinish(session, exitstatus):
|
def pytest_sessionfinish(session, exitstatus):
|
||||||
""" whole test run finishes. """
|
""" 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)
|
# hooks for influencing reporting (invoked from pytest_terminal)
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import difflib
|
||||||
|
import pprint
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -26,3 +29,49 @@ def warn_about_missing_assertion():
|
||||||
else:
|
else:
|
||||||
py.std.warnings.warn("Assertions are turned off!"
|
py.std.warnings.warn("Assertions are turned off!"
|
||||||
" (are you using python -O?)")
|
" (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