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