Author: russellm
Date: 2008-07-19 09:46:55 -0500 (Sat, 19 Jul 2008)
New Revision: 7981

Added:
   django/trunk/tests/regressiontests/test_utils/
   django/trunk/tests/regressiontests/test_utils/__init__.py
   django/trunk/tests/regressiontests/test_utils/models.py
   django/trunk/tests/regressiontests/test_utils/tests.py
Modified:
   django/trunk/django/test/testcases.py
Log:
Fixed #7441 - Improved the doctest OutputChecker to be more lenient with JSON 
an XML outputs. This is required so that output ordering that doesn't matter at 
a semantic level (such as the order of keys in a JSON dictionary, or attributes 
in an XML element) isn't caught as a test failure.  Thanks to Leo Soto for the 
patch.

Modified: django/trunk/django/test/testcases.py
===================================================================
--- django/trunk/django/test/testcases.py       2008-07-19 14:17:24 UTC (rev 
7980)
+++ django/trunk/django/test/testcases.py       2008-07-19 14:46:55 UTC (rev 
7981)
@@ -1,15 +1,17 @@
 import re
 import unittest
 from urlparse import urlsplit, urlunsplit
+from xml.dom.minidom import parseString, Node
 
-from django.http import QueryDict
-from django.db import transaction
 from django.conf import settings
 from django.core import mail
 from django.core.management import call_command
+from django.core.urlresolvers import clear_url_caches
+from django.db import transaction
+from django.http import QueryDict
 from django.test import _doctest as doctest
 from django.test.client import Client
-from django.core.urlresolvers import clear_url_caches
+from django.utils import simplejson
 
 normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
 
@@ -27,16 +29,141 @@
 
 class OutputChecker(doctest.OutputChecker):
     def check_output(self, want, got, optionflags):
-        ok = doctest.OutputChecker.check_output(self, want, got, optionflags)
+        "The entry method for doctest output checking. Defers to a sequence of 
child checkers"
+        checks = (self.check_output_default,
+                  self.check_output_long,
+                  self.check_output_xml,
+                  self.check_output_json)
+        for check in checks:
+            if check(want, got, optionflags):
+                return True
+        return False
 
-        # Doctest does an exact string comparison of output, which means long
-        # integers aren't equal to normal integers ("22L" vs. "22"). The
-        # following code normalizes long integers so that they equal normal
-        # integers.
-        if not ok:
-            return normalize_long_ints(want) == normalize_long_ints(got)
-        return ok
+    def check_output_default(self, want, got, optionflags):
+        "The default comparator provided by doctest - not perfect, but good 
for most purposes"
+        return doctest.OutputChecker.check_output(self, want, got, optionflags)
 
+    def check_output_long(self, want, got, optionflags):
+        """Doctest does an exact string comparison of output, which means long
+        integers aren't equal to normal integers ("22L" vs. "22"). The
+        following code normalizes long integers so that they equal normal
+        integers.
+        """
+        return normalize_long_ints(want) == normalize_long_ints(got)
+
+    def check_output_xml(self, want, got, optionsflags):
+        """Tries to do a 'xml-comparision' of want and got.  Plain string
+        comparision doesn't always work because, for example, attribute
+        ordering should not be important.
+        
+        Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py
+        """
+        
+        # We use this to distinguish the output of repr() from an XML element:
+        _repr_re = re.compile(r'^<[^>]+ (at|object) ')
+
+        _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+')
+        def norm_whitespace(v):
+            return _norm_whitespace_re.sub(' ', v)
+
+        def looks_like_xml(s):
+            s = s.strip()
+            return (s.startswith('<')
+                    and not _repr_re.search(s))
+
+        def child_text(element):
+            return ''.join([c.data for c in element.childNodes
+                            if c.nodeType == Node.TEXT_NODE])
+
+        def children(element):
+            return [c for c in element.childNodes
+                    if c.nodeType == Node.ELEMENT_NODE]
+
+        def norm_child_text(element):
+            return norm_whitespace(child_text(element))
+
+        def attrs_dict(element):
+            return dict(element.attributes.items())
+
+        def check_element(want_element, got_element):
+            if want_element.tagName != got_element.tagName:
+                return False
+            if norm_child_text(want_element) != norm_child_text(got_element):
+                return False
+            if attrs_dict(want_element) != attrs_dict(got_element):
+                return False
+            want_children = children(want_element)
+            got_children = children(got_element)
+            if len(want_children) != len(got_children):
+                return False
+            for want, got in zip(want_children, got_children):
+                if not check_element(want, got):
+                    return False
+            return True
+
+        want, got = self._strip_quotes(want, got)
+        want = want.replace('\\n','\n')
+        got = got.replace('\\n','\n')
+        
+        # If what we want doesn't look like markup, don't bother trying
+        # to parse it.
+        if not looks_like_xml(want):
+            return False
+
+        # Parse the want and got strings, and compare the parsings.
+        try:
+            want_root = parseString(want).firstChild
+            got_root = parseString(got).firstChild
+        except:
+            return False
+        return check_element(want_root, got_root)
+
+    def check_output_json(self, want, got, optionsflags):
+        "Tries to compare want and got as if they were JSON-encoded data"
+        want, got = self._strip_quotes(want, got)
+        try:
+            want_json = simplejson.loads(want)
+            got_json = simplejson.loads(got)
+        except:
+            return False
+        return want_json == got_json
+
+    def _strip_quotes(self, want, got):
+        """
+        Strip quotes of doctests output values:
+
+        >>> o = OutputChecker()
+        >>> o._strip_quotes("'foo'")
+        "foo"
+        >>> o._strip_quotes('"foo"')
+        "foo"
+        >>> o._strip_quotes("u'foo'")
+        "foo"
+        >>> o._strip_quotes('u"foo"')
+        "foo"
+        """
+        def is_quoted_string(s):
+            s = s.strip()
+            return (len(s) >= 2
+                    and s[0] == s[-1]
+                    and s[0] in ('"', "'"))
+
+        def is_quoted_unicode(s):
+            s = s.strip()
+            return (len(s) >= 3
+                    and s[0] == 'u'
+                    and s[1] == s[-1]
+                    and s[1] in ('"', "'"))
+
+        if is_quoted_string(want) and is_quoted_string(got):
+            want = want.strip()[1:-1]
+            got = got.strip()[1:-1]
+        elif is_quoted_unicode(want) and is_quoted_unicode(got):
+            want = want.strip()[2:-1]
+            got = got.strip()[2:-1]
+        return want, got
+
+
 class DocTestRunner(doctest.DocTestRunner):
     def __init__(self, *args, **kwargs):
         doctest.DocTestRunner.__init__(self, *args, **kwargs)

Added: django/trunk/tests/regressiontests/test_utils/__init__.py
===================================================================

Added: django/trunk/tests/regressiontests/test_utils/models.py
===================================================================

Added: django/trunk/tests/regressiontests/test_utils/tests.py
===================================================================
--- django/trunk/tests/regressiontests/test_utils/tests.py                      
        (rev 0)
+++ django/trunk/tests/regressiontests/test_utils/tests.py      2008-07-19 
14:46:55 UTC (rev 7981)
@@ -0,0 +1,57 @@
+r"""
+# Some checks of the doctest output normalizer.
+# Standard doctests do fairly 
+>>> from django.utils import simplejson
+>>> from django.utils.xmlutils import SimplerXMLGenerator
+>>> from StringIO import StringIO
+
+>>> def produce_long():
+...     return 42L
+
+>>> def produce_int():
+...     return 42
+
+>>> def produce_json():
+...     return simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2), 'whiz': 
42}])
+
+>>> def produce_xml():
+...     stream = StringIO()
+...     xml = SimplerXMLGenerator(stream, encoding='utf-8')
+...     xml.startDocument()
+...     xml.startElement("foo", {"aaa" : "1.0", "bbb": "2.0"})
+...     xml.startElement("bar", {"ccc" : "3.0"})
+...     xml.characters("Hello")
+...     xml.endElement("bar")
+...     xml.startElement("whiz", {})
+...     xml.characters("Goodbye")
+...     xml.endElement("whiz")
+...     xml.endElement("foo")
+...     xml.endDocument()
+...     return stream.getvalue()
+
+# Long values are normalized and are comparable to normal integers ...
+>>> produce_long()
+42
+
+# ... and vice versa
+>>> produce_int()
+42L
+
+# JSON output is normalized for field order, so it doesn't matter
+# which order json dictionary attributes are listed in output
+>>> produce_json()
+'["foo", {"bar": ["baz", null, 1.0, 2], "whiz": 42}]'
+
+>>> produce_json()
+'["foo", {"whiz": 42, "bar": ["baz", null, 1.0, 2]}]'
+
+# XML output is normalized for attribute order, so it doesn't matter 
+# which order XML element attributes are listed in output
+>>> produce_xml()
+'<?xml version="1.0" encoding="UTF-8"?>\n<foo aaa="1.0" bbb="2.0"><bar 
ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>'
+
+>>> produce_xml()
+'<?xml version="1.0" encoding="UTF-8"?>\n<foo bbb="2.0" aaa="1.0"><bar 
ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>'
+
+
+"""
\ No newline at end of file


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to