nova/nova/tests/unit/test_matchers.py
Adam Spiers ffd81eb107 fix bug with XML matcher handling missing children
In the XMLMatches matcher used by assertXmlEqual(), it did not
correctly handle the case where a child element of an element <foo>
was expected but the actual element <foo> was totally empty.  In this
scenario, an error like the following would be generated:

    nova.tests.unit.test_matchers.TestXMLMatchesOrderedExtraChildren.test_describe_difference
    -----------------------------------------------------------------------------------------

    Captured traceback:
    ~~~~~~~~~~~~~~~~~~~
        Traceback (most recent call last):
          File ".../nova/.tox/py27/lib/python2.7/site-packages/testtools/tests/matchers/helpers.py", line 32, in test_describe_difference
            mismatch = matcher.match(matchee)
          File "nova/tests/unit/matchers.py", line 431, in match
            actual.getroot(), state, None)
          File "nova/tests/unit/matchers.py", line 520, in _compare_node
            state, actual_child_idx)
          File "nova/tests/unit/matchers.py", line 520, in _compare_node
            state, actual_child_idx)
          File "nova/tests/unit/matchers.py", line 534, in _compare_node
            actual_child_idx + 1)
        UnboundLocalError: local variable 'actual_child_idx' referenced before assignment

actual_child_idx was being used when constructing the XMLExpectedChild
XMLMismatch object, in order to indicate that the missing child was
expected at that position in the list of XML siblings.  However this
was only being set as a side-effect of the loop

    for actual_child_idx in range(...):
        ....

which is brittle in general, but also specifically is guaranteed to
break when the start and end parameters supplied to range() are equal
- in this case actual_child_idx will not get set.

While fixing this reference to the uninitialized variable, it became
apparent that the XMLExpectedChild subclass of XMLMismatch was
generating a slightly misleading error which claimed that the missing
child was expected at a certain index even when the matcher was set
with allow_mixed_nodes=True in order to not require strict ordering of
siblings.

It also became apparent that the use of a context manager for tracking
the XMLMatchState was not appropriate, because the state was stored in
a single object which got mutated as the XML tree was recursed, and
this did not allow for bubbling XMLMismatch objects up the stack which
would accurately preserve the details of the mismatch.

Bearing these issues in mind, make the following changes:

  - Avoid the use of the potentially uninitialized actual_child_idx
    variable.

  - Generate a new XMLMatchState object for each layer of recursion.

  - When an XMLExpectedChild or XMLUnexpectedChild mismatch is
    encountered, bubble that all the way up to the caller.

  - Allow the idx parameter to XMLExpectedChild to be None, when
    strict ordering is not required.

  - Add test cases to cover these scenarios.

Since comparison of XML trees is a generic task, in the future most
likely it would make sense to remove this code from nova, and use
an upstream alternative such as xmldiff:

    https://xmldiff.readthedocs.io/en/stable/

Change-Id: I2dc912d4e3059ab86d414937223f766a1b893900
2019-03-01 18:19:01 +00:00

598 lines
16 KiB
Python

# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from collections import OrderedDict
import pprint
import testtools
from testtools.tests.matchers import helpers
from nova.tests.unit import matchers
class TestDictMatches(testtools.TestCase, helpers.TestMatchersInterface):
matches_dict = OrderedDict(sorted({'foo': 'bar', 'baz': 'DONTCARE',
'cat': {'tabby': True, 'fluffy': False}}.items()))
matches_matcher = matchers.DictMatches(
matches_dict
)
matches_matches = [
{'foo': 'bar', 'baz': 'noox', 'cat': {'tabby': True, 'fluffy': False}},
{'foo': 'bar', 'baz': 'quux', 'cat': {'tabby': True, 'fluffy': False}},
]
matches_mismatches = [
{},
{'foo': 'bar', 'baz': 'qux'},
{'foo': 'bop', 'baz': 'qux',
'cat': {'tabby': True, 'fluffy': False}},
{'foo': 'bar', 'baz': 'quux',
'cat': {'tabby': True, 'fluffy': True}},
{'foo': 'bar', 'cat': {'tabby': True, 'fluffy': False}},
]
str_examples = [
('DictMatches(%s)' % (pprint.pformat(matches_dict)),
matches_matcher),
]
describe_examples = [
("Keys in d1 and not d2: {0}. Keys in d2 and not d1: []"
.format(str(sorted(matches_dict.keys()))), {}, matches_matcher),
("Dictionaries do not match at fluffy. d1: False d2: True",
{'foo': 'bar', 'baz': 'quux',
'cat': {'tabby': True, 'fluffy': True}}, matches_matcher),
("Dictionaries do not match at foo. d1: bar d2: bop",
{'foo': 'bop', 'baz': 'quux',
'cat': {'tabby': True, 'fluffy': False}}, matches_matcher),
]
class TestDictListMatches(testtools.TestCase, helpers.TestMatchersInterface):
matches_matcher = matchers.DictListMatches(
[{'foo': 'bar', 'baz': 'DONTCARE',
'cat': {'tabby': True, 'fluffy': False}},
{'dog': 'yorkie'},
])
matches_matches = [
[{'foo': 'bar', 'baz': 'qoox',
'cat': {'tabby': True, 'fluffy': False}},
{'dog': 'yorkie'}],
[{'foo': 'bar', 'baz': False,
'cat': {'tabby': True, 'fluffy': False}},
{'dog': 'yorkie'}],
]
matches_mismatches = [
[],
{},
[{'foo': 'bar', 'baz': 'qoox',
'cat': {'tabby': True, 'fluffy': True}},
{'dog': 'yorkie'}],
[{'foo': 'bar', 'baz': False,
'cat': {'tabby': True, 'fluffy': False}},
{'cat': 'yorkie'}],
[{'foo': 'bop', 'baz': False,
'cat': {'tabby': True, 'fluffy': False}},
{'dog': 'yorkie'}],
]
str_examples = [
("DictListMatches([{'baz': 'DONTCARE', 'cat':"
" {'fluffy': False, 'tabby': True}, 'foo': 'bar'},\n"
" {'dog': 'yorkie'}])",
matches_matcher),
]
describe_examples = [
("Length mismatch: len(L1)=2 != len(L2)=0", {}, matches_matcher),
("Dictionaries do not match at fluffy. d1: True d2: False",
[{'foo': 'bar', 'baz': 'qoox',
'cat': {'tabby': True, 'fluffy': True}},
{'dog': 'yorkie'}],
matches_matcher),
]
class TestIsSubDictOf(testtools.TestCase, helpers.TestMatchersInterface):
matches_matcher = matchers.IsSubDictOf(
OrderedDict(sorted({'foo': 'bar', 'baz': 'DONTCARE',
'cat': {'tabby': True, 'fluffy': False}}.items()))
)
matches_matches = [
{'foo': 'bar', 'baz': 'noox', 'cat': {'tabby': True, 'fluffy': False}},
{'foo': 'bar', 'baz': 'quux'}
]
matches_mismatches = [
{'foo': 'bop', 'baz': 'qux',
'cat': {'tabby': True, 'fluffy': False}},
{'foo': 'bar', 'baz': 'quux',
'cat': {'tabby': True, 'fluffy': True}},
{'foo': 'bar', 'cat': {'tabby': True, 'fluffy': False}, 'dog': None},
]
str_examples = [
("IsSubDictOf({0})".format(
str(OrderedDict(sorted({'foo': 'bar', 'baz': 'DONTCARE',
'cat': {'tabby': True,
'fluffy': False}}.items())))),
matches_matcher),
]
describe_examples = [
("Dictionaries do not match at fluffy. d1: False d2: True",
{'foo': 'bar', 'baz': 'quux',
'cat': {'tabby': True, 'fluffy': True}}, matches_matcher),
("Dictionaries do not match at foo. d1: bar d2: bop",
{'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>""", allow_mixed_nodes=False)
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>""",
"""<?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-->
<child2>child 2</child2>
<child1>child 1</child1>
<child3>DONTCARE</child3>
<?spam processing instruction?>
</children>
</root>""",
"""<?xml version="1.1"?>
<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>""",
]
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),
("/root/children[3]: XML tag mismatch at index 0: "
"expected tag <child1>; actual tag <child2>",
"""<?xml version="1.0"?>
<root>
<text>some text here</text>
<text>some other text here</text>
<attrs key1="spam" key2="quux"/>
<children>
<child2>child 2</child2>
<child1>child 1</child1>
<child3>child 3</child3>
</children>
</root>""", matches_matcher),
("/: XML information mismatch(version, encoding) "
"expected version 1.0, expected encoding UTF-8; "
"actual version 1.1, actual encoding UTF-8",
"""<?xml version="1.1"?>
<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_matcher),
]
class TestXMLMatchesUnorderedNodes(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>
<child3>DONTCARE</child3>
<!--This is a comment-->
<child2>child 2</child2>
<child1>child 1</child1>
<?spam processing instruction?>
</children>
</root>""", allow_mixed_nodes=True)
matches_matches = ["""<?xml version="1.0"?>
<root>
<text>some text here</text>
<attrs key2="spam" key1="spam"/>
<children>
<child1>child 1</child1>
<child2>child 2</child2>
<child3>child 3</child3>
</children>
<text>some other text here</text>
</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>""",
]
describe_examples = [
("/root: XML expected child element <text> not present",
"""<?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),
]
str_examples = []
class TestXMLMatchesOrderedMissingChildren(testtools.TestCase,
helpers.TestMatchersInterface):
matches_matcher = matchers.XMLMatches("""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2>
<foo>subchild</foo>
</child2>
</children>
</root>""", allow_mixed_nodes=False)
matches_matches = []
matches_mismatches = []
describe_examples = [
("/root/children[0]/child2[1]: XML expected child element <foo> not "
"present at index 0",
"""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2 />
</children>
</root>""", matches_matcher),
]
str_examples = []
class TestXMLMatchesUnorderedMissingChildren(testtools.TestCase,
helpers.TestMatchersInterface):
matches_matcher = matchers.XMLMatches("""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2>
<foo>subchild</foo>
</child2>
</children>
</root>""", allow_mixed_nodes=True)
matches_matches = []
matches_mismatches = []
describe_examples = [
("/root/children[0]/child2[1]: XML expected child element <foo> not "
"present",
"""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2 />
</children>
</root>""", matches_matcher),
]
str_examples = []
class TestXMLMatchesOrderedExtraChildren(testtools.TestCase,
helpers.TestMatchersInterface):
matches_matcher = matchers.XMLMatches("""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2 />
</children>
</root>""", allow_mixed_nodes=False)
matches_matches = []
matches_mismatches = []
describe_examples = [
("/root/children[0]/child2[1]: XML unexpected child element <foo> "
"present at index 0",
"""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2>
<foo>subchild</foo>
</child2>
</children>
</root>""", matches_matcher),
]
str_examples = []
class TestXMLMatchesUnorderedExtraChildren(testtools.TestCase,
helpers.TestMatchersInterface):
matches_matcher = matchers.XMLMatches("""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2 />
</children>
</root>""", allow_mixed_nodes=True)
matches_matches = []
matches_mismatches = []
describe_examples = [
("/root/children[0]/child2[1]: XML unexpected child element <foo> "
"present at index 0",
"""<?xml version="1.0"?>
<root>
<children>
<child1 />
<child2>
<foo>subchild</foo>
</child2>
</children>
</root>""", matches_matcher),
]
str_examples = []