Hello,

With this patch applied and python-PyYAML installed, failed declarative tests will output a diff between the full expected/got structures, so you can see all the context and all the differences at once. (I use YAML because it diffs very nicely, at least with the options used here.)


It's a hack/proof of concept, but I tried extra hard ensure it passes/fails in the same cases as the original assert_deepequal.

If you don't have python-PyYAML installed, the original assert_deepequal is used.

Feel free to use it if it helps.

--
PetrĀ³
From ccaf260b7a5ad0608a3e953925d59080b96f75bc Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Fri, 20 Sep 2013 15:22:30 +0200
Subject: [PATCH] [WIP] ipatest/util: Use YAML-diff for reporting differences
 in assert_deepequal

Unlike the errors from the original assert_deepequal, this format shows
all context and all differences.
The old function is kept as assert_deepequal_original, and checked to
ensure they raise in exactly the same situations.

If the YAML library is not available, the original implementation is used.
---
 ipatests/test_util.py |   2 +-
 ipatests/util.py      | 167 +++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 161 insertions(+), 8 deletions(-)

diff --git a/ipatests/test_util.py b/ipatests/test_util.py
index f87822a223f017f54f8b3bf5f6319b66e97482a7..9562513976c4a9104bdfdd9ae4146bd4056da844 100644
--- a/ipatests/test_util.py
+++ b/ipatests/test_util.py
@@ -130,7 +130,7 @@ def test_eq(self):
 
 
 def test_assert_deepequal():
-    f = util.assert_deepequal
+    f = util.assert_deepequal_original
 
     # Test with good scalar values:
     f(u'hello', u'hello')
diff --git a/ipatests/util.py b/ipatests/util.py
index 30fafdc76c3a139d954c6d7a5ad4c05360fe6de6..4436cab3a70d694edf31361a3d09a125b96f60f3 100644
--- a/ipatests/util.py
+++ b/ipatests/util.py
@@ -24,17 +24,29 @@
 import inspect
 import os
 from os import path
-import ldap
-import ldap.sasl
-import ldap.modlist
+import difflib
+from itertools import izip_longest
 import tempfile
 import shutil
 import re
+import pprint
+
+import ldap
+import ldap.sasl
+import ldap.modlist
+
 import ipalib
 from ipalib.plugable import Plugin
 from ipalib.request import context
 from ipapython.dn import DN
 
+try:
+    import yaml
+except ImportError:
+    have_yaml = False
+else:
+    have_yaml = True
+
 
 class TempDir(object):
     def __init__(self):
@@ -275,7 +287,7 @@ def __ne__(self, other):
   path = %r"""
 
 
-def assert_deepequal(expected, got, doc='', stack=tuple()):
+def assert_deepequal_original(expected, got, doc='', stack=tuple()):
     """
     Recursively check for type and equality.
 
@@ -290,7 +302,7 @@ def assert_deepequal(expected, got, doc='', stack=tuple()):
     >>> got = [u'Hello', dict(world='how are you?')]
     >>> expected == got
     True
-    >>> assert_deepequal(expected, got, doc='Testing my nested data')
+    >>> assert_deepequal_original(expected, got, doc='Testing my nested data')
     Traceback (most recent call last):
       ...
     AssertionError: assert_deepequal: type(expected) is not type(got).
@@ -329,7 +341,7 @@ def assert_deepequal(expected, got, doc='', stack=tuple()):
             s_expected = sorted(expected)
         for (i, e_sub) in enumerate(s_expected):
             g_sub = s_got[i]
-            assert_deepequal(e_sub, g_sub, doc, stack + (i,))
+            assert_deepequal_original(e_sub, g_sub, doc, stack + (i,))
     elif isinstance(expected, dict):
         missing = set(expected).difference(got)
         extra = set(got).difference(expected)
@@ -341,7 +353,7 @@ def assert_deepequal(expected, got, doc='', stack=tuple()):
         for key in sorted(expected):
             e_sub = expected[key]
             g_sub = got[key]
-            assert_deepequal(e_sub, g_sub, doc, stack + (key,))
+            assert_deepequal_original(e_sub, g_sub, doc, stack + (key,))
     elif callable(expected):
         if not expected(got):
             raise AssertionError(
@@ -353,6 +365,147 @@ def assert_deepequal(expected, got, doc='', stack=tuple()):
         )
 
 
+def assert_deepequal_yaml(expected, got, doc='', stack=()):
+    """Recursively check for type and equality
+
+    This works the same as assert_deepequal_original, except it produces
+    more informative messages by showing a YAML diff.
+    The equivalent behavior is checked in every call.
+    """
+    old_expected, old_got = expected, got
+    MISSING = object()
+
+    def yaml_dump(structure):
+        """Dump structure in a particularly diff-friendly style of YAML
+
+        Use pprint as a fallback if yaml.safe_dum fails (e.g. because it
+        can't represent types that aren't "pure data")
+        """
+        try:
+            return yaml.safe_dump(structure, default_flow_style=False)
+        except Exception:
+            return pprint.pformat(structure)
+
+    def normalize(expected, got, stack):
+        """Recursively normalize ``expected`` and ``got`` for comparison & diff
+
+        Because of Fuzzy and custom validation callables we normalize both
+        structures in lockstep. If a custom validator is found, it is called
+        and an error similar to assert_deepequal_orig's is raised.
+
+        In case of missing dict keys or list entries (or Fuzzy/callable
+        checks), the normalization is done with ``expected`` and ``got`` both
+        set to the existing object.
+        This ensures DNs/Fuzzies/callables don't end up in the output.
+        """
+        if isinstance(expected, DN) and isinstance(got, basestring):
+            return type(got)(expected).lower(), got.lower()
+        elif isinstance(expected, DN) and isinstance(got, DN):
+            return unicode(expected).lower(), unicode(got).lower()
+        elif isinstance(expected, (list, tuple)):
+            if not isinstance(got, (list, tuple)):
+                return '<want list or tuple, got %s>' % type(got), got
+            if expected and not isinstance(expected[0], dict):
+                expected = sorted(expected)
+                got = sorted(got)
+            new_expected = []
+            new_got = []
+            for i, (a, b) in enumerate(izip_longest(expected, got,
+                                                    fillvalue=MISSING)):
+                if a is MISSING:
+                    b = normalize(b, b, stack + (i,))[0]
+                    new_got.append(b)
+                elif b is MISSING:
+                    a = normalize(a, a, stack + (i,))[0]
+                    new_expected.append(a)
+                else:
+                    a, b = normalize(a, b, stack + (i,))
+                    new_expected.append(a)
+                    new_got.append(b)
+            return new_expected, new_got
+        elif isinstance(expected, Fuzzy):
+            if expected is got:
+                # This means e.g. the dict entry with a Fuzzy value
+                # is missing from `got`
+                return '<Fuzzy>', '<Fuzzy>'
+            if expected != got:
+                raise AssertionError("Got:\n%s\n\n%s" % (
+                    yaml_dump(old_got),
+                    VALUE % (doc, expected, old_got, stack)))
+            return normalize(got, got, stack)
+        elif callable(expected):
+            if expected is got:
+                # This means e.g. the dict entry with a custom-checked value
+                # is missing from `got`
+                return '<callable>', '<callable>'
+            if not expected(got):
+                raise AssertionError("Got:\n%s\n\n%s" % (
+                    yaml_dump(old_got),
+                    VALUE % (doc, expected, old_got, stack)))
+            return normalize(got, got, stack)
+        elif type(expected) != type(got):
+            # This isn't really normalization but it shows the difference well
+            n_expected = normalize(expected, expected, stack)[0]
+            n_got = normalize(got, got, stack)[0]
+            return ({'__type__': str(type(expected)), '__value__': n_expected},
+                    {'__type__': str(type(got)), '__value__': n_got})
+        elif isinstance(expected, dict):
+            new_expected = []
+            new_got = []
+            for key in sorted(set(expected.keys()) | set(got.keys())):
+                if key not in got:
+                    value = expected[key]
+                    value = normalize(value, value, stack + (key,))[0]
+                    new_expected.append({key: value})
+                elif key not in expected:
+                    value = got[key]
+                    value = normalize(value, value, stack + (key,))[0]
+                    new_got.append({key: value})
+                else:
+                    a, b = expected[key], got[key]
+                    a, b = normalize(a, b, stack + (key,))
+                    new_expected.append({key: a})
+                    new_got.append({key: b})
+            return new_expected, new_got
+        return expected, got
+    expected, got = normalize(expected, got, ())
+    y_expected = yaml_dump(expected)
+    y_got = yaml_dump(got)
+    diff_lines = difflib.unified_diff(y_got.splitlines(True),
+                                      y_expected.splitlines(True),
+                                      'got', 'expected', n=999)
+    if expected == got:
+        if y_expected != y_got:
+            message = ("YAML representation different despite same objects, "
+                       "diff:\n%s")
+        else:
+            try:
+                assert_deepequal_original(old_expected, old_got, doc)
+            except AssertionError, e:
+                message = ("assert_deepequal_original and "
+                           "assert_deepequal_yaml don't agree!"
+                           "\nExpected: \n%s"
+                           "\nDiff: \n%%s"
+                           "\nassert_deepequal error: \n%s"
+                           ) % (y_expected, e)
+        return True
+    else:
+        try:
+            assert_deepequal_original(old_expected, old_got, doc)
+        except AssertionError:
+            message = "expected != got, diff:\n%s"
+        else:
+            message = ("assert_deepequal_original and assert_deepequal_yaml "
+                       "don't agree! Diff:\n%s")
+    raise AssertionError(message % ''.join(diff_lines))
+
+
+if have_yaml:
+    assert_deepequal = assert_deepequal_yaml
+else:
+    assert_deepequal = assert_deepequal_original
+
+
 def raises(exception, callback, *args, **kw):
     """
     Tests that the expected exception is raised; raises ExceptionNotRaised
-- 
1.9.3

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to