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