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.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@7981 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2008-07-19 14:46:55 +00:00
parent c819252bf5
commit 0cffff024b
4 changed files with 195 additions and 11 deletions

View File

@ -1,15 +1,17 @@
import re import re
import unittest import unittest
from urlparse import urlsplit, urlunsplit 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.conf import settings
from django.core import mail from django.core import mail
from django.core.management import call_command 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 import _doctest as doctest
from django.test.client import Client 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) normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
@ -27,15 +29,140 @@ def to_list(value):
class OutputChecker(doctest.OutputChecker): class OutputChecker(doctest.OutputChecker):
def check_output(self, want, got, optionflags): 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
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
# 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
class DocTestRunner(doctest.DocTestRunner): class DocTestRunner(doctest.DocTestRunner):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -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>'
"""