Django

Code

Changeset 7981

Show
Ignore:
Timestamp:
07/19/08 09:46:55 (4 months ago)
Author:
russellm
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/test/testcases.py

    r7805 r7981  
    22import unittest 
    33from urlparse import urlsplit, urlunsplit 
    4  
    5 from django.http import QueryDict 
    6 from django.db import transaction 
     4from xml.dom.minidom import parseString, Node 
     5 
    76from django.conf import settings 
    87from django.core import mail 
    98from django.core.management import call_command 
     9from django.core.urlresolvers import clear_url_caches 
     10from django.db import transaction 
     11from django.http import QueryDict 
    1012from django.test import _doctest as doctest 
    1113from django.test.client import Client 
    12 from django.core.urlresolvers import clear_url_caches 
     14from django.utils import simplejson 
    1315 
    1416normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s) 
     
    2830class OutputChecker(doctest.OutputChecker): 
    2931    def check_output(self, want, got, optionflags): 
    30         ok = doctest.OutputChecker.check_output(self, want, got, optionflags) 
    31  
    32         # Doctest does an exact string comparison of output, which means long 
    33         # integers aren't equal to normal integers ("22L" vs. "22"). The 
    34         # following code normalizes long integers so that they equal normal 
    35         # integers. 
    36         if not ok: 
    37             return normalize_long_ints(want) == normalize_long_ints(got) 
    38         return ok 
     32        "The entry method for doctest output checking. Defers to a sequence of child checkers" 
     33        checks = (self.check_output_default, 
     34                  self.check_output_long, 
     35                  self.check_output_xml, 
     36                  self.check_output_json) 
     37        for check in checks: 
     38            if check(want, got, optionflags): 
     39                return True 
     40        return False 
     41 
     42    def check_output_default(self, want, got, optionflags): 
     43        "The default comparator provided by doctest - not perfect, but good for most purposes" 
     44        return doctest.OutputChecker.check_output(self, want, got, optionflags) 
     45 
     46    def check_output_long(self, want, got, optionflags): 
     47        """Doctest does an exact string comparison of output, which means long 
     48        integers aren't equal to normal integers ("22L" vs. "22"). The 
     49        following code normalizes long integers so that they equal normal 
     50        integers. 
     51        """ 
     52        return normalize_long_ints(want) == normalize_long_ints(got) 
     53 
     54    def check_output_xml(self, want, got, optionsflags): 
     55        """Tries to do a 'xml-comparision' of want and got.  Plain string 
     56        comparision doesn't always work because, for example, attribute 
     57        ordering should not be important. 
     58         
     59        Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py 
     60        """ 
     61         
     62        # We use this to distinguish the output of repr() from an XML element: 
     63        _repr_re = re.compile(r'^<[^>]+ (at|object) ') 
     64 
     65        _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') 
     66        def norm_whitespace(v): 
     67            return _norm_whitespace_re.sub(' ', v) 
     68 
     69        def looks_like_xml(s): 
     70            s = s.strip() 
     71            return (s.startswith('<') 
     72                    and not _repr_re.search(s)) 
     73 
     74        def child_text(element): 
     75            return ''.join([c.data for c in element.childNodes 
     76                            if c.nodeType == Node.TEXT_NODE]) 
     77 
     78        def children(element): 
     79            return [c for c in element.childNodes 
     80                    if c.nodeType == Node.ELEMENT_NODE] 
     81 
     82        def norm_child_text(element): 
     83            return norm_whitespace(child_text(element)) 
     84 
     85        def attrs_dict(element): 
     86            return dict(element.attributes.items()) 
     87 
     88        def check_element(want_element, got_element): 
     89            if want_element.tagName != got_element.tagName: 
     90                return False 
     91            if norm_child_text(want_element) != norm_child_text(got_element): 
     92                return False 
     93            if attrs_dict(want_element) != attrs_dict(got_element): 
     94                return False 
     95            want_children = children(want_element) 
     96            got_children = children(got_element) 
     97            if len(want_children) != len(got_children): 
     98                return False 
     99            for want, got in zip(want_children, got_children): 
     100                if not check_element(want, got): 
     101                    return False 
     102            return True 
     103 
     104        want, got = self._strip_quotes(want, got) 
     105        want = want.replace('\\n','\n') 
     106        got = got.replace('\\n','\n') 
     107         
     108        # If what we want doesn't look like markup, don't bother trying 
     109        # to parse it. 
     110        if not looks_like_xml(want): 
     111            return False 
     112 
     113        # Parse the want and got strings, and compare the parsings. 
     114        try: 
     115            want_root = parseString(want).firstChild 
     116            got_root = parseString(got).firstChild 
     117        except: 
     118            return False 
     119        return check_element(want_root, got_root) 
     120 
     121    def check_output_json(self, want, got, optionsflags): 
     122        "Tries to compare want and got as if they were JSON-encoded data" 
     123        want, got = self._strip_quotes(want, got) 
     124        try: 
     125            want_json = simplejson.loads(want) 
     126            got_json = simplejson.loads(got) 
     127        except: 
     128            return False 
     129        return want_json == got_json 
     130 
     131    def _strip_quotes(self, want, got): 
     132        """ 
     133        Strip quotes of doctests output values: 
     134 
     135        >>> o = OutputChecker() 
     136        >>> o._strip_quotes("'foo'") 
     137        "foo" 
     138        >>> o._strip_quotes('"foo"') 
     139        "foo" 
     140        >>> o._strip_quotes("u'foo'") 
     141        "foo" 
     142        >>> o._strip_quotes('u"foo"') 
     143        "foo" 
     144        """ 
     145        def is_quoted_string(s): 
     146            s = s.strip() 
     147            return (len(s) >= 2 
     148                    and s[0] == s[-1] 
     149                    and s[0] in ('"', "'")) 
     150 
     151        def is_quoted_unicode(s): 
     152            s = s.strip() 
     153            return (len(s) >= 3 
     154                    and s[0] == 'u' 
     155                    and s[1] == s[-1] 
     156                    and s[1] in ('"', "'")) 
     157 
     158        if is_quoted_string(want) and is_quoted_string(got): 
     159            want = want.strip()[1:-1] 
     160            got = got.strip()[1:-1] 
     161        elif is_quoted_unicode(want) and is_quoted_unicode(got): 
     162            want = want.strip()[2:-1] 
     163            got = got.strip()[2:-1] 
     164        return want, got 
     165 
    39166 
    40167class DocTestRunner(doctest.DocTestRunner):