| # Copyright 2006, Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Unit test utilities for gtest_xml_output""" |
| |
| import re |
| from xml.dom import minidom, Node |
| from googletest.test import gtest_test_utils |
| |
| GTEST_DEFAULT_OUTPUT_FILE = 'test_detail.xml' |
| |
| |
| class GTestXMLTestCase(gtest_test_utils.TestCase): |
| """Base class for tests of Google Test's XML output functionality.""" |
| |
| def AssertEquivalentNodes(self, expected_node, actual_node): |
| """Asserts that actual_node is equivalent to expected_node. |
| |
| Asserts that actual_node (a DOM node object) is equivalent to |
| expected_node (another DOM node object), in that either both of |
| them are CDATA nodes and have the same value, or both are DOM |
| elements and actual_node meets all of the following conditions: |
| |
| * It has the same tag name as expected_node. |
| * It has the same set of attributes as expected_node, each with |
| the same value as the corresponding attribute of expected_node. |
| Exceptions are any attribute named "time", which needs only be |
| convertible to a floating-point number and any attribute named |
| "type_param" which only has to be non-empty. |
| * It has an equivalent set of child nodes (including elements and |
| CDATA sections) as expected_node. Note that we ignore the |
| order of the children as they are not guaranteed to be in any |
| particular order. |
| |
| Args: |
| expected_node: expected DOM node object |
| actual_node: actual DOM node object |
| """ |
| |
| if expected_node.nodeType == Node.CDATA_SECTION_NODE: |
| self.assertEqual(Node.CDATA_SECTION_NODE, actual_node.nodeType) |
| self.assertEqual(expected_node.nodeValue, actual_node.nodeValue) |
| return |
| |
| self.assertEqual(Node.ELEMENT_NODE, actual_node.nodeType) |
| self.assertEqual(Node.ELEMENT_NODE, expected_node.nodeType) |
| self.assertEqual(expected_node.tagName, actual_node.tagName) |
| |
| expected_attributes = expected_node.attributes |
| actual_attributes = actual_node.attributes |
| self.assertEqual( |
| expected_attributes.length, |
| actual_attributes.length, |
| 'attribute numbers differ in element %s:\nExpected: %r\nActual: %r' |
| % ( |
| actual_node.tagName, |
| expected_attributes.keys(), |
| actual_attributes.keys(), |
| ), |
| ) |
| for i in range(expected_attributes.length): |
| expected_attr = expected_attributes.item(i) |
| actual_attr = actual_attributes.get(expected_attr.name) |
| self.assertTrue( |
| actual_attr is not None, |
| 'expected attribute %s not found in element %s' |
| % (expected_attr.name, actual_node.tagName), |
| ) |
| self.assertEqual( |
| expected_attr.value, |
| actual_attr.value, |
| ' values of attribute %s in element %s differ: %s vs %s' |
| % ( |
| expected_attr.name, |
| actual_node.tagName, |
| expected_attr.value, |
| actual_attr.value, |
| ), |
| ) |
| |
| expected_children = self._GetChildren(expected_node) |
| actual_children = self._GetChildren(actual_node) |
| self.assertEqual( |
| len(expected_children), |
| len(actual_children), |
| 'number of child elements differ in element ' + actual_node.tagName, |
| ) |
| for child_id, child in expected_children.items(): |
| self.assertTrue( |
| child_id in actual_children, |
| '<%s> is not in <%s> (in element %s)' |
| % (child_id, actual_children, actual_node.tagName), |
| ) |
| self.AssertEquivalentNodes(child, actual_children[child_id]) |
| |
| identifying_attribute = { |
| 'testsuites': 'name', |
| 'testsuite': 'name', |
| 'testcase': 'name', |
| 'failure': 'message', |
| 'skipped': 'message', |
| 'property': 'name', |
| } |
| |
| def _GetChildren(self, element): |
| """Fetches all of the child nodes of element, a DOM Element object. |
| |
| Returns them as the values of a dictionary keyed by the IDs of the children. |
| For <testsuites>, <testsuite>, <testcase>, and <property> elements, the ID |
| is the value of their "name" attribute; for <failure> elements, it is the |
| value of the "message" attribute; for <properties> elements, it is the value |
| of their parent's "name" attribute plus the literal string "properties"; |
| CDATA sections and non-whitespace text nodes are concatenated into a single |
| CDATA section with ID "detail". An exception is raised if any element other |
| than the above four is encountered, if two child elements with the same |
| identifying attributes are encountered, or if any other type of node is |
| encountered. |
| |
| Args: |
| element: DOM Element object |
| |
| Returns: |
| Dictionary where keys are the IDs of the children. |
| """ |
| |
| children = {} |
| for child in element.childNodes: |
| if child.nodeType == Node.ELEMENT_NODE: |
| if child.tagName == 'properties': |
| self.assertTrue( |
| child.parentNode is not None, |
| 'Encountered <properties> element without a parent', |
| ) |
| child_id = child.parentNode.getAttribute('name') + '-properties' |
| else: |
| self.assertTrue( |
| child.tagName in self.identifying_attribute, |
| 'Encountered unknown element <%s>' % child.tagName, |
| ) |
| child_id = child.getAttribute( |
| self.identifying_attribute[child.tagName] |
| ) |
| self.assertNotIn(child_id, children) |
| children[child_id] = child |
| elif child.nodeType in [Node.TEXT_NODE, Node.CDATA_SECTION_NODE]: |
| if 'detail' not in children: |
| if ( |
| child.nodeType == Node.CDATA_SECTION_NODE |
| or not child.nodeValue.isspace() |
| ): |
| children['detail'] = child.ownerDocument.createCDATASection( |
| child.nodeValue |
| ) |
| else: |
| children['detail'].nodeValue += child.nodeValue |
| else: |
| self.fail('Encountered unexpected node type %d' % child.nodeType) |
| return children |
| |
| def NormalizeXml(self, element): |
| """Normalizes XML that may change from run to run. |
| |
| Normalizes Google Test's XML output to eliminate references to transient |
| information that may change from run to run. |
| |
| * The "time" attribute of <testsuites>, <testsuite> and <testcase> |
| elements is replaced with a single asterisk, if it contains |
| only digit characters. |
| * The "timestamp" attribute of <testsuites> elements is replaced with a |
| single asterisk, if it contains a valid ISO8601 datetime value. |
| * The "type_param" attribute of <testcase> elements is replaced with a |
| single asterisk (if it sn non-empty) as it is the type name returned |
| by the compiler and is platform dependent. |
| * The line info reported in the first line of the "message" |
| attribute and CDATA section of <failure> elements is replaced with the |
| file's basename and a single asterisk for the line number. |
| * The directory names in file paths are removed. |
| * The stack traces are removed. |
| |
| Args: |
| element: DOM element to normalize |
| """ |
| |
| if element.tagName == 'testcase': |
| source_file = element.getAttributeNode('file') |
| if source_file: |
| source_file.value = re.sub(r'^.*[/\\](.*)', '\\1', source_file.value) |
| if element.tagName in ('testsuites', 'testsuite', 'testcase'): |
| timestamp = element.getAttributeNode('timestamp') |
| timestamp.value = re.sub( |
| r'^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\d$', '*', timestamp.value |
| ) |
| if element.tagName in ('testsuites', 'testsuite', 'testcase'): |
| time = element.getAttributeNode('time') |
| # The value for exact N seconds has a trailing decimal point (e.g., "10." |
| # instead of "10") |
| time.value = re.sub(r'^\d+\.(\d+)?$', '*', time.value) |
| type_param = element.getAttributeNode('type_param') |
| if type_param and type_param.value: |
| type_param.value = '*' |
| elif element.tagName == 'failure' or element.tagName == 'skipped': |
| source_line_pat = r'^.*[/\\](.*:)\d+\n' |
| # Replaces the source line information with a normalized form. |
| message = element.getAttributeNode('message') |
| message.value = re.sub(source_line_pat, '\\1*\n', message.value) |
| for child in element.childNodes: |
| if child.nodeType == Node.CDATA_SECTION_NODE: |
| # Replaces the source line information with a normalized form. |
| cdata = re.sub(source_line_pat, '\\1*\n', child.nodeValue) |
| # Removes the actual stack trace. |
| child.nodeValue = re.sub( |
| r'Stack trace:\n(.|\n)*', 'Stack trace:\n*', cdata |
| ) |
| for child in element.childNodes: |
| if child.nodeType == Node.ELEMENT_NODE: |
| self.NormalizeXml(child) |