summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-12-12 17:12:59 +0000
committerGerrit Code Review <review@openstack.org>2012-12-12 17:12:59 +0000
commit7ca309c3dab84df0906cb3ad51442e150d50be6e (patch)
tree91ce054cf7c75f4bb199e1e48c4b1e8956e78f0d /nova
parent1b7bf9c93e87f7c7527b596a0e053763a8d826b0 (diff)
parent1b1cd1e6b8a84682f4d40b104f9c87a2f7ff9e1e (diff)
downloadnova-7ca309c3dab84df0906cb3ad51442e150d50be6e.tar.gz
nova-7ca309c3dab84df0906cb3ad51442e150d50be6e.tar.xz
nova-7ca309c3dab84df0906cb3ad51442e150d50be6e.zip
Merge "Implement an XML matcher"
Diffstat (limited to 'nova')
-rw-r--r--nova/tests/matchers.py243
-rw-r--r--nova/tests/test_libvirt_config.py6
-rw-r--r--nova/tests/test_matchers.py207
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 c332ce7c3..8b0340fe8 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),
+ ]