1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 """
37 Library for reading and manipulating Ant JUnit XML result files.
38 """
39
40 from __future__ import print_function
41
42 import os
43 import sys
44 try:
45 from cStringIO import StringIO
46 except ImportError:
47 from io import StringIO
48 import string
49 import codecs
50 import re
51
52 import xml.etree.ElementTree as ET
53 from xml.dom.minidom import parse, parseString
54 from xml.dom import Node as DomNode
55
56 from functools import reduce
57 import rospkg
58
59 invalid_chars = re.compile(ur'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]')
61 return "&#x"+('%04X' % ord(m.group(0)))+";"
64
66 return '<![CDATA[\n{}\n]]>'.format(cdata_text)
67
69 """
70 Common container for 'error' and 'failure' results
71 """
72
74 """
75 @param type_: type attribute from xml
76 @type type_: str
77 @param text: text property from xml
78 @type text: str
79 """
80 self.type = type_
81 self.text = text
82
84 """
85 'error' result container
86 """
88 """
89 @return XML tag representing the object, with non-XML text filtered out
90 @rtype: str
91 """
92 return ET.tostring(self.xml_element(), encoding='utf-8', method='xml')
93
95 """
96 @return XML tag representing the object, with non-XML text filtered out
97 @rtype: xml.etree.ElementTree.Element
98 """
99 error = ET.Element('error')
100 error.set('type', self.type)
101 error.text = cdata(filter_nonprintable_text(self.text))
102 return error
103
105 """
106 'failure' result container
107 """
109 """
110 @return XML tag representing the object, with non-XML text filtered out
111 @rtype: str
112 """
113 return ET.tostring(self.xml_element(), encoding='utf-8', method='xml')
114
116 """
117 @return XML tag representing the object, with non-XML text filtered out
118 @rtype: xml.etree.ElementTree.Element
119 """
120 error = ET.Element('failure')
121 error.set('type', self.type)
122 error.text = cdata(filter_nonprintable_text(self.text))
123 return error
124
126 """
127 'testcase' result container
128 """
129
131 """
132 @param name: name of testcase
133 @type name: str
134 """
135 self.name = name
136 self.failures = []
137 self.errors = []
138 self.time = 0.0
139 self.classname = ''
140
142 """
143 @return: True if test passed
144 @rtype: bool
145 """
146 return not self.errors and not self.failures
147
148 passed = property(_passed)
149
151 """
152 @return: description of testcase failure
153 @rtype: str
154 """
155 if self.failures:
156 tmpl = "[%s][FAILURE]"%self.name
157 tmpl = tmpl + '-'*(80-len(tmpl))
158 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
159 return '\n'.join(tmpl%x.text for x in self.failures)
160 return ''
161
163 """
164 @return: description of testcase error
165 @rtype: str
166 """
167 if self.errors:
168 tmpl = "[%s][ERROR]"%self.name
169 tmpl = tmpl + '-'*(80-len(tmpl))
170 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
171 return '\n'.join(tmpl%x.text for x in self.errors)
172 return ''
173
175 """
176 @return: description of testcase result
177 @rtype: str
178 """
179 if self.passed:
180 return "[%s][passed]\n"%self.name
181 else:
182 return self._failure_description()+\
183 self._error_description()
184
185 description = property(_description)
187 """
188 @param failure TestFailure
189 """
190 self.failures.append(failure)
191
193 """
194 @param failure TestError
195 """
196 self.errors.append(error)
197
199 """
200 @return XML tag representing the object, with non-XML text filtered out
201 @rtype: str
202 """
203 return ET.tostring(self.xml_element(), encoding='utf-8', method='xml')
204
206 """
207 @return XML tag representing the object, with non-XML text filtered out
208 @rtype: xml.etree.ElementTree.Element
209 """
210 testcase = ET.Element('testcase')
211 testcase.set('classname', self.classname)
212 testcase.set('name', self.name)
213 testcase.set('time', str(self.time))
214 for f in self.failures:
215 testcase.append(f.xml_element())
216 for e in self.errors:
217 testcase.append(e.xml_element())
218 return testcase
219
221 __slots__ = ['name', 'num_errors', 'num_failures', 'num_tests', \
222 'test_case_results', 'system_out', 'system_err', 'time']
223 - def __init__(self, name, num_errors=0, num_failures=0, num_tests=0):
232
248
250 """
251 Add results from a testcase to this result container
252 @param r: TestCaseResult
253 @type r: TestCaseResult
254 """
255 self.test_case_results.append(r)
256
274
276 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()
277
279 nodes = [n for n in test_suite.childNodes \
280 if n.nodeType == DomNode.ELEMENT_NODE]
281 for node in nodes:
282 name = node.tagName
283 if name == 'testsuite':
284
285 _load_suite_results(test_suite_name, node, result)
286 elif name == 'system-out':
287 if _text(node):
288 system_out = "[%s] stdout"%test_suite_name + "-"*(71-len(test_suite_name))
289 system_out += '\n'+_text(node)
290 result.system_out += system_out
291 elif name == 'system-err':
292 if _text(node):
293 system_err = "[%s] stderr"%test_suite_name + "-"*(71-len(test_suite_name))
294 system_err += '\n'+_text(node)
295 result.system_err += system_err
296 elif name == 'testcase':
297 name = node.getAttribute('name') or 'unknown'
298 classname = node.getAttribute('classname') or 'unknown'
299
300
301
302 if '__main__.' in classname:
303 classname = classname[classname.find('__main__.')+9:]
304 if classname == 'rostest.rostest.RosTest':
305 classname = 'rostest'
306 elif not classname.startswith(result.name):
307 classname = "%s.%s"%(result.name,classname)
308
309 time = float(node.getAttribute('time')) or 0.0
310 tc_result = TestCaseResult("%s/%s"%(test_suite_name,name))
311 tc_result.classname = classname
312 tc_result.time = time
313 result.add_test_case_result(tc_result)
314 for d in [n for n in node.childNodes \
315 if n.nodeType == DomNode.ELEMENT_NODE]:
316
317
318 if d.tagName == 'failure':
319 message = d.getAttribute('message') or ''
320 text = _text(d) or message
321 x = TestFailure(d.getAttribute('type') or '', text)
322 tc_result.add_failure(x)
323 elif d.tagName == 'error':
324 message = d.getAttribute('message') or ''
325 text = _text(d) or message
326 x = TestError(d.getAttribute('type') or '', text)
327 tc_result.add_error(x)
328
329
330
331
332 try:
333 char = unichr
334 except NameError:
335 char = chr
336 RE_XML_ILLEGAL = '([%s-%s%s-%s%s-%s%s-%s])' + \
337 '|' + \
338 '([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])'
339 try:
340 RE_XML_ILLEGAL = unicode(RE_XML_ILLEGAL)
341 except NameError:
342 pass
343 RE_XML_ILLEGAL = RE_XML_ILLEGAL % \
344 (char(0x0000),char(0x0008),char(0x000b),char(0x000c),
345 char(0x000e),char(0x001f),char(0xfffe),char(0xffff),
346 char(0xd800),char(0xdbff),char(0xdc00),char(0xdfff),
347 char(0xd800),char(0xdbff),char(0xdc00),char(0xdfff),
348 char(0xd800),char(0xdbff),char(0xdc00),char(0xdfff))
349 _safe_xml_regex = re.compile(RE_XML_ILLEGAL)
350
352 """
353 read in file, screen out unsafe unicode characters
354 """
355 f = None
356 try:
357
358
359 if not os.path.isfile(test_file):
360 raise Exception("test file does not exist")
361 try:
362 f = codecs.open(test_file, "r", "utf-8" )
363 x = f.read()
364 except:
365 if f is not None:
366 f.close()
367 f = codecs.open(test_file, "r", "iso8859-1" )
368 x = f.read()
369
370 for match in _safe_xml_regex.finditer(x):
371 x = x[:match.start()] + "?" + x[match.end():]
372 x = x.encode("utf-8")
373 if write_back_sanitized:
374 with open(test_file, 'wb') as h:
375 h.write(x)
376 return x
377 finally:
378 if f is not None:
379 f.close()
380
381 -def read(test_file, test_name):
382 """
383 Read in the test_result file
384 @param test_file: test file path
385 @type test_file: str
386 @param test_name: name of test
387 @type test_name: str
388 @return: test results
389 @rtype: Result
390 """
391 try:
392 xml_str = _read_file_safe_xml(test_file)
393 if not xml_str.strip():
394 print("WARN: test result file is empty [%s]"%(test_file))
395 return Result(test_name, 0, 0, 0)
396 test_suites = parseString(xml_str).getElementsByTagName('testsuite')
397 except Exception as e:
398 print("WARN: cannot read test result file [%s]: %s"%(test_file, str(e)))
399 return Result(test_name, 0, 0, 0)
400 if not test_suites:
401 print("WARN: test result file [%s] contains no results"%(test_file))
402 return Result(test_name, 0, 0, 0)
403
404 results = Result(test_name, 0, 0, 0)
405 for index, test_suite in enumerate(test_suites):
406
407 if index > 0 and test_suite.parentNode in test_suites[0:index]:
408 continue
409
410
411 vals = [test_suite.getAttribute(attr) for attr in ['errors', 'failures', 'tests']]
412 vals = [v or 0 for v in vals]
413 err, fail, tests = [int(val) for val in vals]
414
415 result = Result(test_name, err, fail, tests)
416 result.time = 0.0 if not len(test_suite.getAttribute('time')) else float(test_suite.getAttribute('time'))
417
418
419
420
421 test_file_base = os.path.basename(os.path.dirname(os.path.abspath(test_file)))
422 fname = os.path.basename(test_file)
423 if fname.startswith('TEST-'):
424 fname = fname[5:]
425 if fname.endswith('.xml'):
426 fname = fname[:-4]
427 test_file_base = "%s.%s"%(test_file_base, fname)
428 _load_suite_results(test_file_base, test_suite, result)
429 results.accumulate(result)
430 return results
431
433 """
434 Read in the test_results and aggregate into a single Result object
435 @param filter_: list of packages that should be processed
436 @type filter_: [str]
437 @return: aggregated result
438 @rtype: L{Result}
439 """
440 dir_ = rospkg.get_test_results_dir()
441 root_result = Result('ros', 0, 0, 0)
442 if not os.path.exists(dir_):
443 return root_result
444 for d in os.listdir(dir_):
445 if filter_ and not d in filter_:
446 continue
447 subdir = os.path.join(dir_, d)
448 if os.path.isdir(subdir):
449 for filename in os.listdir(subdir):
450 if filename.endswith('.xml'):
451 filename = os.path.join(subdir, filename)
452 result = read(filename, os.path.basename(subdir))
453 root_result.accumulate(result)
454 return root_result
455
456
458 """
459 Generate JUnit XML file for a unary test suite where the test failed
460
461 @param test_name: Name of test that failed
462 @type test_name: str
463 @param message: failure message
464 @type message: str
465 @param stdout: stdout data to include in report
466 @type stdout: str
467 """
468 testsuite = ET.Element('testsuite')
469 testsuite.set('tests', '1')
470 testsuite.set('failures', '1')
471 testsuite.set('time', '1')
472 testsuite.set('errors', '0')
473 testsuite.set('name', test_name)
474 testcase = ET.SubElement(testsuite, 'testcase')
475 testcase.set('name', 'test_ran')
476 testcase.set('status', 'run')
477 testcase.set('time', '1')
478 testcase.set('classname', 'Results')
479 failure = ET.SubElement(testcase, 'failure')
480 failure.set('message', message)
481 failure.set('type', '')
482 if stdout:
483 system_out = ET.SubElement(testsuite, 'system-out')
484 system_out.text = cdata(filter_nonprintable_text(stdout))
485 return ET.tostring(testsuite, encoding='utf-8', method='xml')
486
488 """
489 Generate JUnit XML file for a unary test suite where the test succeeded.
490
491 @param test_name: Name of test that passed
492 @type test_name: str
493 """
494 testsuite = ET.Element('testsuite')
495 testsuite.set('tests', '1')
496 testsuite.set('failures', '0')
497 testsuite.set('time', '1')
498 testsuite.set('errors', '0')
499 testsuite.set('name', test_name)
500 testcase = ET.SubElement(testsuite, 'testcase')
501 testcase.set('name', 'test_ran')
502 testcase.set('status', 'run')
503 testcase.set('time', '1')
504 testcase.set('classname', 'Results')
505 return ET.tostring(testsuite, encoding='utf-8', method='xml')
506
544