Commit 117e9951 authored by Claude Paroz's avatar Claude Paroz
Browse files

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.
parent 6d46c740
Loading
Loading
Loading
Loading
+35 −89
Original line number Diff line number Diff line
@@ -11,7 +11,6 @@ try:
    from urllib.parse import urlsplit, urlunsplit
except ImportError:     # Python 2
    from urlparse import urlsplit, urlunsplit
from xml.dom.minidom import parseString, Node
import select
import socket
import threading
@@ -38,7 +37,7 @@ from django.test.client import Client
from django.test.html import HTMLParseError, parse_html
from django.test.signals import template_rendered
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.utils import unittest as ut2
from django.utils.encoding import force_text
@@ -134,70 +133,16 @@ class OutputChecker(doctest.OutputChecker):
            optionflags)

    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:
            want_root = parseString(want).firstChild
            got_root = parseString(got).firstChild
            return compare_xml(want, got)
        except Exception:
            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)
        want, got = strip_quotes(want, got)
        try:
            want_json = json.loads(want)
            got_json = json.loads(got)
@@ -205,37 +150,6 @@ class OutputChecker(doctest.OutputChecker):
            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"
        """
        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):
@@ -445,6 +359,38 @@ class SimpleTestCase(ut2.TestCase):
                safe_repr(dom1, True), safe_repr(dom2, True))
            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):

+92 −0
Original line number Diff line number Diff line
import re
import warnings
from xml.dom.minidom import parseString, Node

from django.conf import settings, UserSettingsHolder
from django.core import mail
from django.test.signals import template_rendered, setting_changed
@@ -223,5 +226,94 @@ class override_settings(object):
                                 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):
    return s % {'_': '' if six.PY3 else 'u'}
+5 −0
Original line number Diff line number Diff line
@@ -198,6 +198,11 @@ Django 1.5 also includes several smaller improvements worth noting:
* The loaddata management command now supports an `ignorenonexistent` option to
  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
=====================================

+19 −0
Original line number Diff line number Diff line
@@ -1783,6 +1783,25 @@ your test suite.
    ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be
    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:

Email services
+35 −0
Original line number Diff line number Diff line
@@ -450,6 +450,41 @@ class HTMLEqualTests(TestCase):
        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):
    fixtures = ['should_not_be_loaded.json']