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 import cStringIO
45 import string
46 import codecs
47 import re
48
49 from xml.dom.minidom import parse, parseString
50 from xml.dom import Node as DomNode
51
52 import roslib.rosenv
53
55 """
56 Common container for 'error' and 'failure' results
57 """
58
60 """
61 @param type_: type attribute from xml
62 @type type_: str
63 @param text: text property from xml
64 @type text: str
65 """
66 self.type = type_
67 self.text = text
68
70 """
71 'error' result container
72 """
74 return u'<error type="%s"><![CDATA[%s]]></error>'%(self.type, self.text)
75
77 """
78 'failure' result container
79 """
81 return u'<failure type="%s"><![CDATA[%s]]></failure>'%(self.type, self.text)
82
83
85 """
86 'testcase' result container
87 """
88
90 """
91 @param name: name of testcase
92 @type name: str
93 """
94 self.name = name
95 self.failures = []
96 self.errors = []
97 self.time = 0.0
98 self.classname = ''
99
101 """
102 @return: True if test passed
103 @rtype: bool
104 """
105 return not self.errors and not self.failures
106
107 passed = property(_passed)
108
110 """
111 @return: description of testcase failure
112 @rtype: str
113 """
114 if self.failures:
115 tmpl = "[%s][FAILURE]"%self.name
116 tmpl = tmpl + '-'*(80-len(tmpl))
117 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
118 return '\n'.join(tmpl%x.text for x in self.failures)
119 return ''
120
122 """
123 @return: description of testcase error
124 @rtype: str
125 """
126 if self.errors:
127 tmpl = "[%s][ERROR]"%self.name
128 tmpl = tmpl + '-'*(80-len(tmpl))
129 tmpl = tmpl+"\n%s\n"+'-'*80+"\n\n"
130 return '\n'.join(tmpl%x.text for x in self.errors)
131 return ''
132
134 """
135 @return: description of testcase result
136 @rtype: str
137 """
138 if self.passed:
139 return "[%s][passed]\n"%self.name
140 else:
141 return self._failure_description()+\
142 self._error_description()
143
144 description = property(_description)
146 """
147 @param failure TestFailure
148 """
149 self.failures.append(failure)
150
152 """
153 @param failure TestError
154 """
155 self.errors.append(error)
156
158 return u' <testcase classname="%s" name="%s" time="%s">\n'%(self.classname, self.name, self.time)+\
159 '\n '.join([f.xml() for f in self.failures])+\
160 '\n '.join([e.xml() for e in self.errors])+\
161 ' </testcase>'
162
164 __slots__ = ['name', 'num_errors', 'num_failures', 'num_tests', \
165 'test_case_results', 'system_out', 'system_err', 'time']
166 - def __init__(self, name, num_errors=0, num_failures=0, num_tests=0):
175
191
193 """
194 Add results from a testcase to this result container
195 @param r: TestCaseResult
196 @type r: TestCaseResult
197 """
198 self.test_case_results.append(r)
199
201 """
202 @return: document as unicode (UTF-8 declared) XML according to Ant JUnit spec
203 """
204 return u'<?xml version="1.0" encoding="utf-8"?>'+\
205 '<testsuite name="%s" tests="%s" errors="%s" failures="%s" time="%s">'%\
206 (self.name, self.num_tests, self.num_errors, self.num_failures, self.time)+\
207 '\n'.join([tc.xml() for tc in self.test_case_results])+\
208 ' <system-out><![CDATA[%s]]></system-out>'%self.system_out+\
209 ' <system-err><![CDATA[%s]]></system-err>'%self.system_err+\
210 '</testsuite>'
211
213 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()
214
216 nodes = [n for n in test_suite.childNodes \
217 if n.nodeType == DomNode.ELEMENT_NODE]
218 for node in nodes:
219 name = node.tagName
220 if name == 'testsuite':
221
222 _load_suite_results(test_suite_name, node, result)
223 elif name == 'system-out':
224 if _text(node):
225 system_out = "[%s] stdout"%test_suite_name + "-"*(71-len(test_suite_name))
226 system_out += '\n'+_text(node)
227 result.system_out += system_out
228 elif name == 'system-err':
229 if _text(node):
230 system_err = "[%s] stderr"%test_suite_name + "-"*(71-len(test_suite_name))
231 system_err += '\n'+_text(node)
232 result.system_err += system_err
233 elif name == 'testcase':
234 name = node.getAttribute('name') or 'unknown'
235 classname = node.getAttribute('classname') or 'unknown'
236
237
238
239 if '__main__.' in classname:
240 classname = classname[classname.find('__main__.')+9:]
241 if classname == 'rostest.rostest.RosTest':
242 classname = 'rostest'
243 elif not classname.startswith(result.name):
244 classname = "%s.%s"%(result.name,classname)
245
246 time = float(node.getAttribute('time')) or 0.0
247 tc_result = TestCaseResult("%s/%s"%(test_suite_name,name))
248 tc_result.classname = classname
249 tc_result.time = time
250 result.add_test_case_result(tc_result)
251 for d in [n for n in node.childNodes \
252 if n.nodeType == DomNode.ELEMENT_NODE]:
253
254
255 if d.tagName == 'failure':
256 message = d.getAttribute('message') or ''
257 text = _text(d) or message
258 x = TestFailure(d.getAttribute('type') or '', text)
259 tc_result.add_failure(x)
260 elif d.tagName == 'error':
261 message = d.getAttribute('message') or ''
262 text = _text(d) or message
263 x = TestError(d.getAttribute('type') or '', text)
264 tc_result.add_error(x)
265
266
267
268
269 RE_XML_ILLEGAL = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \
270 u'|' + \
271 u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \
272 (unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
273 unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff),
274 unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff))
275 _safe_xml_regex = re.compile(RE_XML_ILLEGAL)
276
278 """
279 read in file, screen out unsafe unicode characters
280 """
281 f = None
282 try:
283
284
285 if not os.path.isfile(test_file):
286 raise Exception("test file does not exist")
287 try:
288 f = codecs.open(test_file, "r", "utf-8" )
289 x = f.read()
290 except:
291 if f is not None:
292 f.close()
293 f = codecs.open(test_file, "r", "iso8859-1" )
294 x = f.read()
295
296 for match in _safe_xml_regex.finditer(x):
297 x = x[:match.start()] + "?" + x[match.end():]
298 return x.encode("utf-8")
299 finally:
300 if f is not None:
301 f.close()
302
303 -def read(test_file, test_name):
304 """
305 Read in the test_result file
306 @param test_file: test file path
307 @type test_file: str
308 @param test_name: name of test
309 @type test_name: str
310 @return: test results
311 @rtype: Result
312 """
313 try:
314 xml_str = _read_file_safe_xml(test_file)
315 if not xml_str.strip():
316 print("WARN: test result file is empty [%s]"%(test_file))
317 return Result(test_name, 0, 0, 0)
318 test_suites = parseString(xml_str).getElementsByTagName('testsuite')
319 except Exception as e:
320 print("WARN: cannot read test result file [%s]: %s"%(test_file, str(e)))
321 return Result(test_name, 0, 0, 0)
322 if not test_suites:
323 print("WARN: test result file [%s] contains no results"%(test_file))
324 return Result(test_name, 0, 0, 0)
325
326 results = Result(test_name, 0, 0, 0)
327 for test_suite in test_suites:
328
329 vals = [test_suite.getAttribute(attr) for attr in ['errors', 'failures', 'tests']]
330 vals = [v or 0 for v in vals]
331 err, fail, tests = [string.atoi(val) for val in vals]
332
333 result = Result(test_name, err, fail, tests)
334 result.time = float(test_suite.getAttribute('time')) or 0.0
335
336
337
338
339 test_file_base = os.path.basename(os.path.dirname(os.path.abspath(test_file)))
340 fname = os.path.basename(test_file)
341 if fname.startswith('TEST-'):
342 fname = fname[5:]
343 if fname.endswith('.xml'):
344 fname = fname[:-4]
345 test_file_base = "%s.%s"%(test_file_base, fname)
346 _load_suite_results(test_file_base, test_suite, result)
347 results.accumulate(result)
348 return results
349
351 """
352 Read in the test_results and aggregate into a single Result object
353 @param filter_: list of packages that should be processed
354 @type filter_: [str]
355 @return: aggregated result
356 @rtype: L{Result}
357 """
358 dir_ = roslib.rosenv.get_test_results_dir()
359 root_result = Result('ros', 0, 0, 0)
360 if not os.path.exists(dir_):
361 return root_result
362 for d in os.listdir(dir_):
363 if filter_ and not d in filter_:
364 continue
365 subdir = os.path.join(dir_, d)
366 if os.path.isdir(subdir):
367 for filename in os.listdir(subdir):
368 if filename.endswith('.xml'):
369 filename = os.path.join(subdir, filename)
370 result = read(filename, os.path.basename(subdir))
371 root_result.accumulate(result)
372 return root_result
373
374
376 """
377 Generate JUnit XML file for a unary test suite where the test failed
378
379 @param test_name: Name of test that failed
380 @type test_name: str
381 @param message: failure message
382 @type message: str
383 @param stdout: stdout data to include in report
384 @type stdout: str
385 """
386 if not stdout:
387 return """<?xml version="1.0" encoding="UTF-8"?>
388 <testsuite tests="1" failures="1" time="1" errors="0" name="%s">
389 <testcase name="test_ran" status="run" time="1" classname="Results">
390 <failure message="%s" type=""/>
391 </testcase>
392 </testsuite>"""%(test_name, message)
393 else:
394 return """<?xml version="1.0" encoding="UTF-8"?>
395 <testsuite tests="1" failures="1" time="1" errors="0" name="%s">
396 <testcase name="test_ran" status="run" time="1" classname="Results">
397 <failure message="%s" type=""/>
398 </testcase>
399 <system-out><![CDATA[[
400 %s
401 ]]></system-out>
402 </testsuite>"""%(test_name, message, stdout)
403
405 """
406 Generate JUnit XML file for a unary test suite where the test succeeded.
407
408 @param test_name: Name of test that passed
409 @type test_name: str
410 """
411 return """<?xml version="1.0" encoding="UTF-8"?>
412 <testsuite tests="1" failures="0" time="1" errors="0" name="%s">
413 <testcase name="test_ran" status="run" time="1" classname="Results">
414 </testcase>
415 </testsuite>"""%(test_name)
416
454