Kaydet (Commit) 117e9951 authored tarafından Claude Paroz's avatar Claude Paroz

Added assertXML[Not]Equal assertions

This is especially needed to compare XML when hash randomization
is on, as attribute order may vary. Refs #17758, #19038.
Thanks Taylor Mitchell for the initial patch, and Ian Clelland for
review and cleanup.
üst 6d46c740
...@@ -11,7 +11,6 @@ try: ...@@ -11,7 +11,6 @@ try:
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
except ImportError: # Python 2 except ImportError: # Python 2
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
from xml.dom.minidom import parseString, Node
import select import select
import socket import socket
import threading import threading
...@@ -38,7 +37,7 @@ from django.test.client import Client ...@@ -38,7 +37,7 @@ from django.test.client import Client
from django.test.html import HTMLParseError, parse_html from django.test.html import HTMLParseError, parse_html
from django.test.signals import template_rendered from django.test.signals import template_rendered
from django.test.utils import (get_warnings_state, restore_warnings_state, from django.test.utils import (get_warnings_state, restore_warnings_state,
override_settings) override_settings, compare_xml, strip_quotes)
from django.test.utils import ContextList from django.test.utils import ContextList
from django.utils import unittest as ut2 from django.utils import unittest as ut2
from django.utils.encoding import force_text from django.utils.encoding import force_text
...@@ -134,70 +133,16 @@ class OutputChecker(doctest.OutputChecker): ...@@ -134,70 +133,16 @@ class OutputChecker(doctest.OutputChecker):
optionflags) optionflags)
def check_output_xml(self, want, got, optionsflags): 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
"""
_norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+')
def norm_whitespace(v):
return _norm_whitespace_re.sub(' ', v)
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 the string is not a complete xml document, we may need to add a
# root element. This allow us to compare fragments, like "<foo/><bar/>"
if not want.startswith('<?xml'):
wrapper = '<root>%s</root>'
want = wrapper % want
got = wrapper % got
# Parse the want and got strings, and compare the parsings.
try: try:
want_root = parseString(want).firstChild return compare_xml(want, got)
got_root = parseString(got).firstChild
except Exception: except Exception:
return False return False
return check_element(want_root, got_root)
def check_output_json(self, want, got, optionsflags): def check_output_json(self, want, got, optionsflags):
""" """
Tries to compare want and got as if they were JSON-encoded data Tries to compare want and got as if they were JSON-encoded data
""" """
want, got = self._strip_quotes(want, got) want, got = strip_quotes(want, got)
try: try:
want_json = json.loads(want) want_json = json.loads(want)
got_json = json.loads(got) got_json = json.loads(got)
...@@ -205,37 +150,6 @@ class OutputChecker(doctest.OutputChecker): ...@@ -205,37 +150,6 @@ class OutputChecker(doctest.OutputChecker):
return False return False
return want_json == got_json 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"
"""
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): class DocTestRunner(doctest.DocTestRunner):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -445,6 +359,38 @@ class SimpleTestCase(ut2.TestCase): ...@@ -445,6 +359,38 @@ class SimpleTestCase(ut2.TestCase):
safe_repr(dom1, True), safe_repr(dom2, True)) safe_repr(dom1, True), safe_repr(dom2, True))
self.fail(self._formatMessage(msg, standardMsg)) self.fail(self._formatMessage(msg, standardMsg))
def assertXMLEqual(self, xml1, xml2, msg=None):
"""
Asserts that two XML snippets are semantically the same.
Whitespace in most cases is ignored, and attribute ordering is not
significant. The passed-in arguments must be valid XML.
"""
try:
result = compare_xml(xml1, xml2)
except Exception as e:
standardMsg = 'First or second argument is not valid XML\n%s' % e
self.fail(self._formatMessage(msg, standardMsg))
else:
if not result:
standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True))
self.fail(self._formatMessage(msg, standardMsg))
def assertXMLNotEqual(self, xml1, xml2, msg=None):
"""
Asserts that two XML snippets are not semantically equivalent.
Whitespace in most cases is ignored, and attribute ordering is not
significant. The passed-in arguments must be valid XML.
"""
try:
result = compare_xml(xml1, xml2)
except Exception as e:
standardMsg = 'First or second argument is not valid XML\n%s' % e
self.fail(self._formatMessage(msg, standardMsg))
else:
if result:
standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True))
self.fail(self._formatMessage(msg, standardMsg))
class TransactionTestCase(SimpleTestCase): class TransactionTestCase(SimpleTestCase):
......
import re
import warnings import warnings
from xml.dom.minidom import parseString, Node
from django.conf import settings, UserSettingsHolder from django.conf import settings, UserSettingsHolder
from django.core import mail from django.core import mail
from django.test.signals import template_rendered, setting_changed from django.test.signals import template_rendered, setting_changed
...@@ -223,5 +226,94 @@ class override_settings(object): ...@@ -223,5 +226,94 @@ class override_settings(object):
setting=key, value=new_value) setting=key, value=new_value)
def compare_xml(want, got):
"""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
"""
_norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+')
def norm_whitespace(v):
return _norm_whitespace_re.sub(' ', v)
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 = strip_quotes(want, got)
want = want.replace('\\n','\n')
got = got.replace('\\n','\n')
# If the string is not a complete xml document, we may need to add a
# root element. This allow us to compare fragments, like "<foo/><bar/>"
if not want.startswith('<?xml'):
wrapper = '<root>%s</root>'
want = wrapper % want
got = wrapper % got
# Parse the want and got strings, and compare the parsings.
want_root = parseString(want).firstChild
got_root = parseString(got).firstChild
return check_element(want_root, got_root)
def strip_quotes(want, got):
"""
Strip quotes of doctests output values:
>>> strip_quotes("'foo'")
"foo"
>>> strip_quotes('"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
def str_prefix(s): def str_prefix(s):
return s % {'_': '' if six.PY3 else 'u'} return s % {'_': '' if six.PY3 else 'u'}
...@@ -198,6 +198,11 @@ Django 1.5 also includes several smaller improvements worth noting: ...@@ -198,6 +198,11 @@ Django 1.5 also includes several smaller improvements worth noting:
* The loaddata management command now supports an `ignorenonexistent` option to * The loaddata management command now supports an `ignorenonexistent` option to
ignore data for fields that no longer exist. ignore data for fields that no longer exist.
* :meth:`~django.test.SimpleTestCase.assertXMLEqual` and
:meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow
you to test equality for XML content at a semantic level, without caring for
syntax differences (spaces, attribute order, etc.).
Backwards incompatible changes in 1.5 Backwards incompatible changes in 1.5
===================================== =====================================
......
...@@ -1783,6 +1783,25 @@ your test suite. ...@@ -1783,6 +1783,25 @@ your test suite.
``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be
raised if one of them cannot be parsed. raised if one of them cannot be parsed.
.. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)
.. versionadded:: 1.5
Asserts that the strings ``xml1`` and ``xml2`` are equal. The
comparison is based on XML semantics. Similarily to
:meth:`~SimpleTestCase.assertHTMLEqual`, the comparison is
made on parsed content, hence only semantic differences are considered, not
syntax differences. When unvalid XML is passed in any parameter, an
``AssertionError`` is always raised, even if both string are identical.
.. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)
.. versionadded:: 1.5
Asserts that the strings ``xml1`` and ``xml2`` are *not* equal. The
comparison is based on XML semantics. See
:meth:`~SimpleTestCase.assertXMLEqual` for details.
.. _topics-testing-email: .. _topics-testing-email:
Email services Email services
......
...@@ -450,6 +450,41 @@ class HTMLEqualTests(TestCase): ...@@ -450,6 +450,41 @@ class HTMLEqualTests(TestCase):
self.assertContains(response, '<p class="help">Some help text for the title (with unicode ŠĐĆŽćžšđ)</p>', html=True) self.assertContains(response, '<p class="help">Some help text for the title (with unicode ŠĐĆŽćžšđ)</p>', html=True)
class XMLEqualTests(TestCase):
def test_simple_equal(self):
xml1 = "<elem attr1='a' attr2='b' />"
xml2 = "<elem attr1='a' attr2='b' />"
self.assertXMLEqual(xml1, xml2)
def test_simple_equal_unordered(self):
xml1 = "<elem attr1='a' attr2='b' />"
xml2 = "<elem attr2='b' attr1='a' />"
self.assertXMLEqual(xml1, xml2)
def test_simple_equal_raise(self):
xml1 = "<elem attr1='a' />"
xml2 = "<elem attr2='b' attr1='a' />"
with self.assertRaises(AssertionError):
self.assertXMLEqual(xml1, xml2)
def test_simple_not_equal(self):
xml1 = "<elem attr1='a' attr2='c' />"
xml2 = "<elem attr1='a' attr2='b' />"
self.assertXMLNotEqual(xml1, xml2)
def test_simple_not_equal_raise(self):
xml1 = "<elem attr1='a' attr2='b' />"
xml2 = "<elem attr2='b' attr1='a' />"
with self.assertRaises(AssertionError):
self.assertXMLNotEqual(xml1, xml2)
def test_parsing_errors(self):
xml_unvalid = "<elem attr1='a attr2='b' />"
xml2 = "<elem attr2='b' attr1='a' />"
with self.assertRaises(AssertionError):
self.assertXMLNotEqual(xml_unvalid, xml2)
class SkippingExtraTests(TestCase): class SkippingExtraTests(TestCase):
fixtures = ['should_not_be_loaded.json'] fixtures = ['should_not_be_loaded.json']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment