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

Reply via email to