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@codespeak.net > http://codespeak.net/mailman/listinfo/py-dev -- _______________________________________________ py-dev mailing list py-dev@codespeak.net http://codespeak.net/mailman/listinfo/py-dev