Hi Ever since unittest grew it's .assertSequenceEqual() and .assertMultilineEqual() I've been jealous of it. So this weekend I've looked into the py.test code and made an attempt at getting this into my favourite testing tool.
The attached patch makes compare equal a special case and checks if the two arguments to it are both a list, text or dict and tries to generate a nicer explanation text for them. The patch is more like a proof of concept then a final implementation, I may have done some very strange or silly things as I'm not familiar with the code. It would be great to get feedback, both on the general concept and the actual implementation (particularly note the way I had to hack _format_explanation() in assertion.py). Some of the rough edges I can think off right now: (i) no idea how comparisons and nested calls work together, (ii) no attempt is made to limit the output from difflib so the screen doesn't get flooded. There's probably many more. I hope this can be useful Floris -- Debian GNU/Linux -- The Power of Freedom www.debian.org | www.gnu.org | www.kernel.org
diff --git a/py/_code/_assertionnew.py b/py/_code/_assertionnew.py --- a/py/_code/_assertionnew.py +++ b/py/_code/_assertionnew.py @@ -5,6 +5,8 @@ This should replace _assertionold.py eve import sys import ast +import difflib +import pprint import py from py._code.assertion import _format_explanation, BuiltinAssertionError @@ -164,8 +166,6 @@ class DebugInterpreter(ast.NodeVisitor): 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,11 +177,56 @@ 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 + if op_symbol == "==": + new_expl = self._explain_equal(left_result, next_result, + left_explanation, next_explanation) + if new_expl: + explanation = new_expl return explanation, result + def _explain_equal(self, left, right, left_repr, right_repr): + """Make a specialised explanation for comapare equal""" + if type(left) != type(right): + return None + explanation = [] + if len(left_repr) > 30: + left_repr = left_repr[:27] + '...' + if len(right_repr) > 30: + right_repr = right_repr[:27] + '...' + 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 issquence(left): + 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)]] + explanation += [line.strip('\n') for line in + difflib.ndiff(pprint.pformat(left).splitlines(), + pprint.pformat(right).splitlines())] + elif istext(left): + explanation += [line.strip('\n') for line in + difflib.ndiff(left.splitlines(), + right.splitlines())] + elif isdict(left): + explanation += [line.strip('\n') for line in + difflib.ndiff(pprint.pformat(left).splitlines(), + pprint.pformat(right).splitlines())] + else: + return None # No specialised knowledge + return '\n=='.join(explanation) + def visit_BoolOp(self, boolop): is_or = isinstance(boolop.op, ast.Or) explanations = [] diff --git a/py/_code/assertion.py b/py/_code/assertion.py --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -10,7 +10,7 @@ def _format_explanation(explanation): # 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 +28,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(' ' + line.strip('==')) assert len(stack) == 1 return '\n'.join(result) diff --git a/testing/code/test_assertionnew.py b/testing/code/test_assertionnew.py new file mode 100644 --- /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
_______________________________________________ py-dev mailing list py-dev@codespeak.net http://codespeak.net/mailman/listinfo/py-dev