diff options
| -rw-r--r-- | nova/tests/matchers.py | 243 | ||||
| -rw-r--r-- | nova/tests/test_libvirt_config.py | 6 | ||||
| -rw-r--r-- | nova/tests/test_matchers.py | 207 |
3 files changed, 452 insertions, 4 deletions
diff --git a/nova/tests/matchers.py b/nova/tests/matchers.py index c3b88d2e5..a421cc056 100644 --- a/nova/tests/matchers.py +++ b/nova/tests/matchers.py @@ -20,6 +20,8 @@ import pprint +from lxml import etree + class DictKeysMismatch(object): def __init__(self, d1only, d2only): @@ -194,3 +196,244 @@ class IsSubDictOf(object): else: if sub_value != super_value: return SubDictMismatch(k, sub_value, super_value) + + +class XMLMismatch(object): + """Superclass for XML mismatch.""" + + def __init__(self, state): + self.path = str(state) + self.expected = state.expected + self.actual = state.actual + + def describe(self): + return "%(path)s: XML does not match" % self.__dict__ + + def get_details(self): + return { + 'expected': self.expected, + 'actual': self.actual, + } + + +class XMLTagMismatch(XMLMismatch): + """XML tags don't match.""" + + def __init__(self, state, idx, expected_tag, actual_tag): + super(XMLTagMismatch, self).__init__(state) + self.idx = idx + self.expected_tag = expected_tag + self.actual_tag = actual_tag + + def describe(self): + return ("%(path)s: XML tag mismatch at index %(idx)d: " + "expected tag <%(expected_tag)s>; " + "actual tag <%(actual_tag)s>" % self.__dict__) + + +class XMLAttrKeysMismatch(XMLMismatch): + """XML attribute keys don't match.""" + + def __init__(self, state, expected_only, actual_only): + super(XMLAttrKeysMismatch, self).__init__(state) + self.expected_only = ', '.join(sorted(expected_only)) + self.actual_only = ', '.join(sorted(actual_only)) + + def describe(self): + return ("%(path)s: XML attributes mismatch: " + "keys only in expected: %(expected_only)s; " + "keys only in actual: %(actual_only)s" % self.__dict__) + + +class XMLAttrValueMismatch(XMLMismatch): + """XML attribute values don't match.""" + + def __init__(self, state, key, expected_value, actual_value): + super(XMLAttrValueMismatch, self).__init__(state) + self.key = key + self.expected_value = expected_value + self.actual_value = actual_value + + def describe(self): + return ("%(path)s: XML attribute value mismatch: " + "expected value of attribute %(key)s: %(expected_value)r; " + "actual value: %(actual_value)r" % self.__dict__) + + +class XMLTextValueMismatch(XMLMismatch): + """XML text values don't match.""" + + def __init__(self, state, expected_text, actual_text): + super(XMLTextValueMismatch, self).__init__(state) + self.expected_text = expected_text + self.actual_text = actual_text + + def describe(self): + return ("%(path)s: XML text value mismatch: " + "expected text value: %(expected_text)r; " + "actual value: %(actual_text)r" % self.__dict__) + + +class XMLUnexpectedChild(XMLMismatch): + """Unexpected child present in XML.""" + + def __init__(self, state, tag, idx): + super(XMLUnexpectedChild, self).__init__(state) + self.tag = tag + self.idx = idx + + def describe(self): + return ("%(path)s: XML unexpected child element <%(tag)s> " + "present at index %(idx)d" % self.__dict__) + + +class XMLExpectedChild(XMLMismatch): + """Expected child not present in XML.""" + + def __init__(self, state, tag, idx): + super(XMLExpectedChild, self).__init__(state) + self.tag = tag + self.idx = idx + + def describe(self): + return ("%(path)s: XML expected child element <%(tag)s> " + "not present at index %(idx)d" % self.__dict__) + + +class XMLMatchState(object): + """ + Maintain some state for matching. + + Tracks the XML node path and saves the expected and actual full + XML text, for use by the XMLMismatch subclasses. + """ + + def __init__(self, expected, actual): + self.path = [] + self.expected = expected + self.actual = actual + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, exc_tb): + self.path.pop() + return False + + def __str__(self): + return '/' + '/'.join(self.path) + + def node(self, tag, idx): + """ + Adds tag and index to the path; they will be popped off when + the corresponding 'with' statement exits. + + :param tag: The element tag + :param idx: If not None, the integer index of the element + within its parent. Not included in the path + element if None. + """ + + if idx is not None: + self.path.append("%s[%d]" % (tag, idx)) + else: + self.path.append(tag) + return self + + +class XMLMatches(object): + """Compare XML strings. More complete than string comparison.""" + + def __init__(self, expected): + self.expected_xml = expected + self.expected = etree.fromstring(expected) + + def __str__(self): + return 'XMLMatches(%r)' % self.expected_xml + + def match(self, actual_xml): + actual = etree.fromstring(actual_xml) + + state = XMLMatchState(self.expected_xml, actual_xml) + result = self._compare_node(self.expected, actual, state, None) + + if result is False: + return XMLMismatch(state) + elif result is not True: + return result + + def _compare_node(self, expected, actual, state, idx): + """Recursively compares nodes within the XML tree.""" + + # Start by comparing the tags + if expected.tag != actual.tag: + return XMLTagMismatch(state, idx, expected.tag, actual.tag) + + with state.node(expected.tag, idx): + # Compare the attribute keys + expected_attrs = set(expected.attrib.keys()) + actual_attrs = set(actual.attrib.keys()) + if expected_attrs != actual_attrs: + expected_only = expected_attrs - actual_attrs + actual_only = actual_attrs - expected_attrs + return XMLAttrKeysMismatch(state, expected_only, actual_only) + + # Compare the attribute values + for key in expected_attrs: + expected_value = expected.attrib[key] + actual_value = actual.attrib[key] + + if 'DONTCARE' in (expected_value, actual_value): + continue + elif expected_value != actual_value: + return XMLAttrValueMismatch(state, key, expected_value, + actual_value) + + # Compare the contents of the node + if len(expected) == 0 and len(actual) == 0: + # No children, compare text values + if ('DONTCARE' not in (expected.text, actual.text) and + expected.text != actual.text): + return XMLTextValueMismatch(state, expected.text, + actual.text) + else: + expected_idx = 0 + actual_idx = 0 + while (expected_idx < len(expected) and + actual_idx < len(actual)): + # Ignore comments and processing instructions + # TODO(Vek): may interpret PIs in the future, to + # allow for, say, arbitrary ordering of some + # elements + if (expected[expected_idx].tag in + (etree.Comment, etree.ProcessingInstruction)): + expected_idx += 1 + continue + + # Compare the nodes + result = self._compare_node(expected[expected_idx], + actual[actual_idx], state, + actual_idx) + if result is not True: + return result + + # Step on to comparing the next nodes... + expected_idx += 1 + actual_idx += 1 + + # Make sure we consumed all nodes in actual + if actual_idx < len(actual): + return XMLUnexpectedChild(state, actual[actual_idx].tag, + actual_idx) + + # Make sure we consumed all nodes in expected + if expected_idx < len(expected): + for node in expected[expected_idx:]: + if (node.tag in + (etree.Comment, etree.ProcessingInstruction)): + continue + + return XMLExpectedChild(state, node.tag, actual_idx) + + # The nodes match + return True diff --git a/nova/tests/test_libvirt_config.py b/nova/tests/test_libvirt_config.py index c285d46c0..11881f377 100644 --- a/nova/tests/test_libvirt_config.py +++ b/nova/tests/test_libvirt_config.py @@ -18,15 +18,13 @@ from lxml import etree from lxml import objectify from nova import test - +from nova.tests import matchers from nova.virt.libvirt import config class LibvirtConfigBaseTest(test.TestCase): def assertXmlEqual(self, expectedXmlstr, actualXmlstr): - expected = etree.tostring(objectify.fromstring(expectedXmlstr)) - actual = etree.tostring(objectify.fromstring(actualXmlstr)) - self.assertEqual(expected, actual) + self.assertThat(actualXmlstr, matchers.XMLMatches(expectedXmlstr)) class LibvirtConfigTest(LibvirtConfigBaseTest): diff --git a/nova/tests/test_matchers.py b/nova/tests/test_matchers.py index b764b3d45..be058aa7d 100644 --- a/nova/tests/test_matchers.py +++ b/nova/tests/test_matchers.py @@ -142,3 +142,210 @@ class TestDictMatches(testtools.TestCase, helpers.TestMatchersInterface): {'foo': 'bop', 'baz': 'quux', 'cat': {'tabby': True, 'fluffy': False}}, matches_matcher), ] + + +class TestXMLMatches(testtools.TestCase, helpers.TestMatchersInterface): + + matches_matcher = matchers.XMLMatches("""<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="DONTCARE"/> + <children> + <!--This is a comment--> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>DONTCARE</child3> + <?spam processing instruction?> + </children> +</root>""") + + matches_matches = ["""<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key2="spam" key1="spam"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children><child1>child 1</child1> +<child2>child 2</child2> +<child3>blah</child3> + </children> +</root>""", + ] + + matches_mismatches = ["""<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>mismatch text</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key3="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="quux" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child4>child 4</child4> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + </children> +</root>""", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + <child4>child 4</child4> + </children> +</root>""", + ] + + str_examples = [ + ("XMLMatches('<?xml version=\"1.0\"?>\\n" + "<root>\\n" + " <text>some text here</text>\\n" + " <text>some other text here</text>\\n" + " <attrs key1=\"spam\" key2=\"DONTCARE\"/>\\n" + " <children>\\n" + " <!--This is a comment-->\\n" + " <child1>child 1</child1>\\n" + " <child2>child 2</child2>\\n" + " <child3>DONTCARE</child3>\\n" + " <?spam processing instruction?>\\n" + " </children>\\n" + "</root>')", matches_matcher), + ] + + describe_examples = [ + ("/root/text[1]: XML text value mismatch: expected text value: " + "'some other text here'; actual value: 'mismatch text'", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>mismatch text</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", matches_matcher), + ("/root/attrs[2]: XML attributes mismatch: keys only in expected: " + "key2; keys only in actual: key3", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key3="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", matches_matcher), + ("/root/attrs[2]: XML attribute value mismatch: expected value of " + "attribute key1: 'spam'; actual value: 'quux'", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="quux" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", matches_matcher), + ("/root/children[3]: XML tag mismatch at index 1: expected tag " + "<child2>; actual tag <child4>", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child4>child 4</child4> + <child2>child 2</child2> + <child3>child 3</child3> + </children> +</root>""", matches_matcher), + ("/root/children[3]: XML expected child element <child3> not " + "present at index 2", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + </children> +</root>""", matches_matcher), + ("/root/children[3]: XML unexpected child element <child4> " + "present at index 3", + """<?xml version="1.0"?> +<root> + <text>some text here</text> + <text>some other text here</text> + <attrs key1="spam" key2="quux"/> + <children> + <child1>child 1</child1> + <child2>child 2</child2> + <child3>child 3</child3> + <child4>child 4</child4> + </children> +</root>""", matches_matcher), + ] |
