Merge "Fixes an issue with the XMLEquals matcher"
This commit is contained in:
commit
a8b844251b
keystone/tests
@ -12,8 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import io
|
||||
|
||||
from lxml import etree
|
||||
from testtools import matchers
|
||||
|
||||
@ -30,24 +28,50 @@ class XMLEquals(object):
|
||||
return "%s(%r)" % (self.__class__.__name__, self.expected)
|
||||
|
||||
def match(self, other):
|
||||
def xml_element_equals(expected_doc, observed_doc):
|
||||
"""Tests whether two XML documents are equivalent.
|
||||
|
||||
This is a recursive algorithm that operates on each element in
|
||||
the hierarchy. Siblings are sorted before being checked to
|
||||
account for two semantically equivalent documents where siblings
|
||||
appear in different document order.
|
||||
|
||||
The sorting algorithm is a little weak in that it could fail for
|
||||
documents where siblings at a given level are the same, but have
|
||||
different children.
|
||||
|
||||
"""
|
||||
|
||||
if expected_doc.tag != observed_doc.tag:
|
||||
return False
|
||||
|
||||
if expected_doc.attrib != observed_doc.attrib:
|
||||
return False
|
||||
|
||||
def _sorted_children(doc):
|
||||
return sorted(doc.getchildren(), key=lambda el: el.tag)
|
||||
|
||||
expected_children = _sorted_children(expected_doc)
|
||||
observed_children = _sorted_children(observed_doc)
|
||||
|
||||
if len(expected_children) != len(observed_children):
|
||||
return False
|
||||
|
||||
for expected_el, observed_el in zip(expected_children,
|
||||
observed_children):
|
||||
if not xml_element_equals(expected_el, observed_el):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
expected_doc = etree.fromstring(self.expected.strip(), parser)
|
||||
observed_doc = etree.fromstring(other.strip(), parser)
|
||||
|
||||
def canonical_xml(s):
|
||||
s = s.strip()
|
||||
|
||||
fp = io.BytesIO()
|
||||
dom = etree.fromstring(s, parser)
|
||||
dom.getroottree().write_c14n(fp)
|
||||
s = fp.getvalue()
|
||||
|
||||
dom = etree.fromstring(s, parser)
|
||||
return etree.tostring(dom, pretty_print=True).decode('utf-8')
|
||||
|
||||
expected = canonical_xml(self.expected)
|
||||
other = canonical_xml(other)
|
||||
if expected == other:
|
||||
if xml_element_equals(expected_doc, observed_doc):
|
||||
return
|
||||
return XMLMismatch(expected, other)
|
||||
|
||||
return XMLMismatch(self.expected, other)
|
||||
|
||||
|
||||
class XMLMismatch(matchers.Mismatch):
|
||||
@ -57,4 +81,11 @@ class XMLMismatch(matchers.Mismatch):
|
||||
self.other = other
|
||||
|
||||
def describe(self):
|
||||
return 'expected = %s\nactual = %s' % (self.expected, self.other)
|
||||
def pretty_xml(xml):
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
doc = etree.fromstring(xml.strip(), parser)
|
||||
return (etree.tostring(doc, encoding='utf-8', pretty_print=True)
|
||||
.decode('utf-8'))
|
||||
|
||||
return 'expected =\n%s\nactual =\n%s' % (
|
||||
pretty_xml(self.expected), pretty_xml(self.other))
|
||||
|
@ -12,8 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import textwrap
|
||||
|
||||
from testtools.tests.matchers import helpers
|
||||
|
||||
from keystone import tests
|
||||
@ -21,33 +19,38 @@ from keystone.tests import matchers
|
||||
|
||||
|
||||
class TestXMLEquals(tests.BaseTestCase, helpers.TestMatchersInterface):
|
||||
matches_xml = b"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<success a="a" b="b"/>
|
||||
</test>
|
||||
"""
|
||||
equivalent_xml = b"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<success b="b" a="a"></success>
|
||||
</test>
|
||||
"""
|
||||
mismatches_xml = b"""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<nope_it_fails/>
|
||||
</test>
|
||||
"""
|
||||
mismatches_description = textwrap.dedent("""\
|
||||
expected = <test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<success a="a" b="b"/>
|
||||
</test>
|
||||
matches_xml = b"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<first z="0" y="1" x="2"/>
|
||||
<second a="a" b="b"></second>
|
||||
</test>
|
||||
"""
|
||||
equivalent_xml = b"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<second a="a" b="b"/>
|
||||
<first z="0" y="1" x="2"></first>
|
||||
</test>
|
||||
"""
|
||||
mismatches_xml = b"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<nope_it_fails/>
|
||||
</test>
|
||||
"""
|
||||
mismatches_description = """\
|
||||
expected =
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<first z="0" y="1" x="2"/>
|
||||
<second a="a" b="b"/>
|
||||
</test>
|
||||
|
||||
actual = <test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<nope_it_fails/>
|
||||
</test>
|
||||
""").lstrip()
|
||||
actual =
|
||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||
<nope_it_fails/>
|
||||
</test>
|
||||
"""
|
||||
|
||||
matches_matcher = matchers.XMLEquals(matches_xml)
|
||||
matches_matches = [matches_xml, equivalent_xml]
|
||||
|
Loading…
x
Reference in New Issue
Block a user