From cd013746cfa054f5115c8a4b25293b102a17b230 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Sep 2010 19:35:17 +0100 Subject: [PATCH] Initial patch as sent to py-dev With a small but disasterous typo fixed though. --HG-- branch : trunk --- py/_code/_assertionnew.py | 14 +++--- py/_code/assertion.py | 19 ++++++-- py/_plugin/hookspec.py | 13 ++++++ py/_plugin/pytest_assertion.py | 49 ++++++++++++++++++++ testing/code/test_assertionnew.py | 74 +++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 testing/code/test_assertionnew.py diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py index 0c7b0090e..a80ae898c 100644 --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -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): diff --git a/py/_code/assertion.py b/py/_code/assertion.py index 2a2da9cfb..558ee740e 100644 --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -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) diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 04e1bd0d2..f295211cd 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -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) # ------------------------------------------------------------------------- diff --git a/py/_plugin/pytest_assertion.py b/py/_plugin/pytest_assertion.py index f18350e7c..5e9c2b81b 100644 --- a/py/_plugin/pytest_assertion.py +++ b/py/_plugin/pytest_assertion.py @@ -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())] diff --git a/testing/code/test_assertionnew.py b/testing/code/test_assertionnew.py new file mode 100644 index 000000000..ebc0edf77 --- /dev/null +++ b/testing/code/test_assertionnew.py @@ -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