Package rostest :: Module xmlresults

Source Code for Module rostest.xmlresults

  1  #!/usr/bin/env python 
  2  # Software License Agreement (BSD License) 
  3  # 
  4  # Copyright (c) 2008, Willow Garage, Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions 
  9  # are met: 
 10  # 
 11  #  * Redistributions of source code must retain the above copyright 
 12  #    notice, this list of conditions and the following disclaimer. 
 13  #  * Redistributions in binary form must reproduce the above 
 14  #    copyright notice, this list of conditions and the following 
 15  #    disclaimer in the documentation and/or other materials provided 
 16  #    with the distribution. 
 17  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 18  #    contributors may be used to endorse or promote products derived 
 19  #    from this software without specific prior written permission. 
 20  # 
 21  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 22  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 23  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 24  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 25  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 26  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 27  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 28  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 29  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 30  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 31  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 32  # POSSIBILITY OF SUCH DAMAGE. 
 33  # 
 34  # Revision $Id: xmlresults.py 13232 2011-02-14 06:14:50Z kwc $ 
 35   
 36  import codecs 
 37  import os 
 38  import sys 
 39  import string 
 40  from xml.dom.minidom import parse, parseString 
 41  from xml.dom import Node as DomNode 
 42   
 43  import roslib.rosenv 
 44   
 45  ## Common container for 'error' and 'failure' results 
46 -class _TestInfo(object):
47 ## @param type str: type attribute from xml 48 ## @param text str: text property from xml
49 - def __init__(self, type, text):
50 self.type = type 51 self.text = text
52 ## 'error' result container
53 -class TestError(_TestInfo):
54 - def xml(self):
55 return u'<error type="%s"><![CDATA[%s]]></error>'%(self.type, self.text)
56 57 ## 'failure' result container
58 -class TestFailure(_TestInfo):
59 - def xml(self):
60 return u'<failure type="%s"><![CDATA[%s]]></failure>'%(self.type, self.text)
61 62 63 ## 'testcase' result container
64 -class TestCaseResult(object):
65 ## @param name str: name of testcase
66 - def __init__(self, name):
67 self.name = name 68 self.failures = [] 69 self.errors = [] 70 self.time = 0.0 71 self.classname = ''
72 73 ## @return bool: True if test passed
74 - def _passed(self):
75 return not self.errors and not self.failures
76 ## bool: True if test passed without errors or failures 77 passed = property(_passed) 78 79 ## @return str: description of testcase failure
80 - def _failure_description(self):
81 if self.failures: 82 tmpl = "[%s][FAILURE]"%self.name 83 tmpl = tmpl + '-'*(80-len(tmpl)) 84 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n" 85 return '\n'.join(tmpl%x.text for x in self.failures) 86 return ''
87 ## @return str: description of testcase error
88 - def _error_description(self):
89 if self.errors: 90 tmpl = "[%s][ERROR]"%self.name 91 tmpl = tmpl + '-'*(80-len(tmpl)) 92 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n" 93 return '\n'.join(tmpl%x.text for x in self.errors) 94 return ''
95 ## @return str: description of testcase result
96 - def _description(self):
97 if self.passed: 98 return "[%s][passed]\n"%self.name 99 else: 100 return self._failure_description()+\ 101 self._error_description()
102 ## str: printable description of testcase result 103 description = property(_description) 104 ## @param failure TestFailure
105 - def add_failure(self, failure):
106 self.failures.append(failure)
107 ## @param failure TestError
108 - def add_error(self, error):
109 self.errors.append(error)
110
111 - def xml(self):
112 return u' <testcase classname="%s" name="%s" time="%s">\n'%(self.classname, self.name, self.time)+\ 113 '\n '.join([f.xml() for f in self.failures])+\ 114 '\n '.join([e.xml() for e in self.errors])+\ 115 ' </testcase>'
116
117 -class Result(object):
118 __slots__ = ['name', 'num_errors', 'num_failures', 'num_tests', \ 119 'test_case_results', 'system_out', 'system_err', 'time']
120 - def __init__(self, name, num_errors, num_failures, num_tests):
121 self.name = name 122 self.num_errors = num_errors 123 self.num_failures = num_failures 124 self.num_tests = num_tests 125 self.test_case_results = [] 126 self.system_out = '' 127 self.system_err = '' 128 self.time = 0.0
129 130 ## Add results from \a r to this result 131 ## @param r Result: results to aggregate with this result
132 - def accumulate(self, r):
133 self.num_errors += r.num_errors 134 self.num_failures += r.num_failures 135 self.num_tests += r.num_tests 136 self.test_case_results.extend(r.test_case_results) 137 if r.system_out: 138 self.system_out += '\n'+r.system_out 139 if r.system_err: 140 self.system_err += '\n'+r.system_err
141 142 ## Add results from a testcase to this result container 143 ## @param r TestCaseResult
144 - def add_test_case_result(self, r):
145 self.test_case_results.append(r)
146 147 ## @return document as unicode (UTF-8 declared) XML
148 - def xml(self):
149 return u'<?xml version="1.0" encoding="utf-8"?>'+\ 150 '<testsuite name="%s" tests="%s" errors="%s" failures="%s" time="%s">'%\ 151 (self.name, self.num_tests, self.num_errors, self.num_failures, self.time)+\ 152 '\n'.join([tc.xml() for tc in self.test_case_results])+\ 153 ' <system-out><![CDATA[%s]]></system-out>'%self.system_out+\ 154 ' <system-err><![CDATA[%s]]></system-err>'%self.system_err+\ 155 '</testsuite>'
156
157 -def _text(tag):
158 return reduce(lambda x, y: x + y, [c.data for c in tag.childNodes if c.nodeType in [DomNode.TEXT_NODE, DomNode.CDATA_SECTION_NODE]], "").strip()
159
160 -def _load_suite_results(test_suite_name, test_suite, result):
161 nodes = [n for n in test_suite.childNodes \ 162 if n.nodeType == DomNode.ELEMENT_NODE] 163 for node in nodes: 164 name = node.tagName 165 if name == 'testsuite': 166 # for now we flatten this hierarchy 167 _load_suite_results(test_suite_name, node, result) 168 elif name == 'system-out': 169 if _text(node): 170 system_out = "[%s] stdout"%test_suite_name + "-"*(71-len(test_suite_name)) 171 system_out += '\n'+_text(node) 172 result.system_out += system_out 173 elif name == 'system-err': 174 if _text(node): 175 system_err = "[%s] stderr"%test_suite_name + "-"*(71-len(test_suite_name)) 176 system_err += '\n'+_text(node) 177 result.system_err += system_err 178 elif name == 'testcase': 179 name = node.getAttribute('name') or 'unknown' 180 classname = node.getAttribute('classname') or 'unknown' 181 182 # mangle the classname for some sense of uniformity 183 # between rostest/unittest/gtest 184 if '__main__.' in classname: 185 classname = classname[classname.find('__main__.')+9:] 186 if classname == 'rostest.rostest.RosTest': 187 classname = 'rostest' 188 elif not classname.startswith(result.name): 189 classname = "%s.%s"%(result.name,classname) 190 191 time = node.getAttribute('time') or 0.0 192 tc_result = TestCaseResult("%s/%s"%(test_suite_name,name)) 193 tc_result.classname = classname 194 tc_result.time = time 195 result.add_test_case_result(tc_result) 196 for d in [n for n in node.childNodes \ 197 if n.nodeType == DomNode.ELEMENT_NODE]: 198 # convert 'message' attributes to text elements to keep 199 # python unittest and gtest consistent 200 if d.tagName == 'failure': 201 message = d.getAttribute('message') or '' 202 text = _text(d) or message 203 x = TestFailure(d.getAttribute('type') or '', text) 204 tc_result.add_failure(x) 205 elif d.tagName == 'error': 206 message = d.getAttribute('message') or '' 207 text = _text(d) or message 208 x = TestError(d.getAttribute('type') or '', text) 209 tc_result.add_error(x)
210 211 ## #603: unit test suites are not good about screening out illegal 212 ## unicode characters. This little recipe I from http://boodebr.org/main/python/all-about-python-and-unicode#UNI_XML 213 ## screens these out 214 import re 215 RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \ 216 u'|' + \ 217 u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \ 218 (unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), 219 unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), 220 unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff)) 221 _safe_xml_regex = re.compile(RE_XML_ILLEGAL) 222 223 ## read in file, screen out unsafe unicode characters
224 -def _read_file_safe_xml(test_file):
225 f = None 226 try: 227 # this is ugly, but the files in question that are problematic 228 # do not declare unicode type. 229 if not os.path.isfile(test_file): 230 raise Exception("test file does not exist") 231 try: 232 f = codecs.open(test_file, "r", "utf-8" ) 233 x = f.read() 234 except: 235 if f is not None: 236 f.close() 237 f = codecs.open(test_file, "r", "iso8859-1" ) 238 x = f.read() 239 240 for match in _safe_xml_regex.finditer(x): 241 x = x[:match.start()] + "?" + x[match.end():] 242 return x.encode("utf-8") 243 finally: 244 if f is not None: 245 f.close()
246 247 ## Read in the test_result file 248 ## @param test_file str: test file path 249 ## @param test_name str: name of test 250 ## @return Result test results
251 -def read(test_file, test_name):
252 try: 253 xml_str = _read_file_safe_xml(test_file) 254 if not xml_str.strip(): 255 print "WARN: test result file is empty [%s]"%(test_file) 256 return Result(test_name, 0, 0, 0) 257 test_suite = parseString(xml_str).getElementsByTagName('testsuite') 258 except Exception as e: 259 print >> sys.stderr, str(e) 260 print "WARN: cannot read test result file [%s]: %s"%(test_file, str(e)) 261 return Result(test_name, 0, 0, 0) 262 if not test_suite: 263 print "WARN: test result file [%s] contains no results"%test_file 264 return Result(test_name, 0, 0, 0) 265 test_suite = test_suite[0] 266 vals = [test_suite.getAttribute(attr) for attr in ['errors', 'failures', 'tests']] 267 vals = [v or 0 for v in vals] 268 err, fail, tests = [string.atoi(val) for val in vals] 269 270 result = Result(test_name, err, fail, tests) 271 result.time = test_suite.getAttribute('time') or 0.0 272 273 # Create a prefix based on the test result filename. The idea is to 274 # disambiguate the case when tests of the same name are provided in 275 # different .xml files. We use the name of the parent directory 276 test_file_base = os.path.basename(os.path.dirname(test_file)) 277 fname = os.path.basename(test_file) 278 if fname.startswith('TEST-'): 279 fname = fname[5:] 280 if fname.endswith('.xml'): 281 fname = fname[:-4] 282 test_file_base = "%s.%s"%(test_file_base, fname) 283 _load_suite_results(test_file_base, test_suite, result) 284 return result
285
286 -def read_all(filter=[]):
287 """ 288 Read in the test_results and aggregate into a single Result object 289 @param filter: list of packages that should be processed 290 @type filter: [str] 291 @return: aggregated result 292 @rtype: L{Result} 293 """ 294 dir_ = roslib.rosenv.get_test_results_dir() 295 root_result = Result('ros', 0, 0, 0) 296 if not os.path.exists(dir_): 297 return root_result 298 for d in os.listdir(dir_): 299 if filter and not d in filter: 300 continue 301 subdir = os.path.join(dir_, d) 302 if os.path.isdir(subdir): 303 for file in os.listdir(subdir): 304 if file.endswith('.xml'): 305 file = os.path.join(subdir, file) 306 result = read(file, os.path.basename(subdir)) 307 root_result.accumulate(result) 308 return root_result
309