Fixes an issue with the XMLEquals matcher
The matcher implementation would fail to match two documents that are semantically equivalent, but sibling elements appear in different document order. Change-Id: I99dc6401e73be4c61bb265c3258b6245f2e7bb34 Closes-bug: #1347891
This commit is contained in:
parent
409c94d616
commit
c4ec6eb424
@ -12,8 +12,6 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import io
|
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
|
|
||||||
@ -30,24 +28,50 @@ class XMLEquals(object):
|
|||||||
return "%s(%r)" % (self.__class__.__name__, self.expected)
|
return "%s(%r)" % (self.__class__.__name__, self.expected)
|
||||||
|
|
||||||
def match(self, other):
|
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)
|
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):
|
if xml_element_equals(expected_doc, observed_doc):
|
||||||
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:
|
|
||||||
return
|
return
|
||||||
return XMLMismatch(expected, other)
|
|
||||||
|
return XMLMismatch(self.expected, other)
|
||||||
|
|
||||||
|
|
||||||
class XMLMismatch(matchers.Mismatch):
|
class XMLMismatch(matchers.Mismatch):
|
||||||
@ -57,4 +81,11 @@ class XMLMismatch(matchers.Mismatch):
|
|||||||
self.other = other
|
self.other = other
|
||||||
|
|
||||||
def describe(self):
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
from testtools.tests.matchers import helpers
|
from testtools.tests.matchers import helpers
|
||||||
|
|
||||||
from keystone import tests
|
from keystone import tests
|
||||||
@ -21,33 +19,38 @@ from keystone.tests import matchers
|
|||||||
|
|
||||||
|
|
||||||
class TestXMLEquals(tests.BaseTestCase, helpers.TestMatchersInterface):
|
class TestXMLEquals(tests.BaseTestCase, helpers.TestMatchersInterface):
|
||||||
matches_xml = b"""
|
matches_xml = b"""\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||||
<success a="a" b="b"/>
|
<first z="0" y="1" x="2"/>
|
||||||
</test>
|
<second a="a" b="b"></second>
|
||||||
"""
|
</test>
|
||||||
equivalent_xml = b"""
|
"""
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
equivalent_xml = b"""\
|
||||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<success b="b" a="a"></success>
|
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||||
</test>
|
<second a="a" b="b"/>
|
||||||
"""
|
<first z="0" y="1" x="2"></first>
|
||||||
mismatches_xml = b"""
|
</test>
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
"""
|
||||||
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
mismatches_xml = b"""\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||||
<nope_it_fails/>
|
<nope_it_fails/>
|
||||||
</test>
|
</test>
|
||||||
"""
|
"""
|
||||||
mismatches_description = textwrap.dedent("""\
|
mismatches_description = """\
|
||||||
expected = <test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
expected =
|
||||||
<success a="a" b="b"/>
|
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||||
</test>
|
<first z="0" y="1" x="2"/>
|
||||||
|
<second a="a" b="b"/>
|
||||||
|
</test>
|
||||||
|
|
||||||
actual = <test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
actual =
|
||||||
|
<test xmlns="http://docs.openstack.org/identity/api/v2.0">
|
||||||
<nope_it_fails/>
|
<nope_it_fails/>
|
||||||
</test>
|
</test>
|
||||||
""").lstrip()
|
"""
|
||||||
|
|
||||||
matches_matcher = matchers.XMLEquals(matches_xml)
|
matches_matcher = matchers.XMLEquals(matches_xml)
|
||||||
matches_matches = [matches_xml, equivalent_xml]
|
matches_matches = [matches_xml, equivalent_xml]
|
||||||
|
Loading…
Reference in New Issue
Block a user