From c4ec6eb42402d703b53e8a59f38040af395f3c56 Mon Sep 17 00:00:00 2001 From: David Stanek Date: Thu, 24 Jul 2014 02:38:34 +0000 Subject: [PATCH] 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 --- keystone/tests/matchers.py | 67 ++++++++++++++++++++++++--------- keystone/tests/test_matchers.py | 59 +++++++++++++++-------------- 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/keystone/tests/matchers.py b/keystone/tests/matchers.py index 1b928c0c9..6076f348e 100644 --- a/keystone/tests/matchers.py +++ b/keystone/tests/matchers.py @@ -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)) diff --git a/keystone/tests/test_matchers.py b/keystone/tests/test_matchers.py index d4215f1ce..e9413a1f6 100644 --- a/keystone/tests/test_matchers.py +++ b/keystone/tests/test_matchers.py @@ -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""" - - - - - """ - equivalent_xml = b""" - - - - - """ - mismatches_xml = b""" - - - - - """ - mismatches_description = textwrap.dedent("""\ - expected = - - + matches_xml = b"""\ + + + + + +""" + equivalent_xml = b"""\ + + + + + +""" + mismatches_xml = b"""\ + + + + +""" + mismatches_description = """\ +expected = + + + + - actual = - - - """).lstrip() +actual = + + + +""" matches_matcher = matchers.XMLEquals(matches_xml) matches_matches = [matches_xml, equivalent_xml]