[py-dev] Advanced assert equal

holger krekel holger at merlinux.eu
Mon Aug 16 09:10:16 CEST 2010


Hey Floris,

nice.  i also thought about improving reporting for particular types
of assert-expressions.  Will take a look at your code after
holiday and maybe Benjamin can also take a look or make a suggestion
on how to best make assert expression-reporting customizable. 

cheers,
holger

On Mon, Aug 16, 2010 at 01:25 +0100, Floris Bruynooghe wrote:
> 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 at codespeak.net
> http://codespeak.net/mailman/listinfo/py-dev


-- 



More information about the Pytest-dev mailing list