2 The LLVM Compiler Infrastructure
4 This file is distributed under the University of Illinois Open Source
5 License. See LICENSE.TXT for details.
7 Provides an xUnit ResultsFormatter for integrating the LLDB
8 test suite with the Jenkins xUnit aggregator and other xUnit-compliant
9 test output processors.
11 from __future__ import absolute_import
12 from __future__ import print_function
17 import xml.sax.saxutils
23 from ..event_builder import EventBuilder
24 from ..build_exception import BuildError
25 from .results_formatter import ResultsFormatter
28 class XunitFormatter(ResultsFormatter):
29 """Provides xUnit-style formatted output.
32 # Result mapping arguments
34 RM_SUCCESS = 'success'
35 RM_FAILURE = 'failure'
36 RM_PASSTHRU = 'passthru'
39 def _build_illegal_xml_regex():
40 """Constructs a regex to match all illegal xml characters.
42 Expects to be used against a unicode string."""
43 # Construct the range pairs of invalid unicode characters.
45 (0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84),
46 (0x86, 0x9F), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)]
48 # For wide builds, we have more.
49 if sys.maxunicode >= 0x10000:
50 illegal_chars_u.extend(
51 [(0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF),
52 (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF),
53 (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF),
54 (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF),
55 (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF),
56 (0x10FFFE, 0x10FFFF)])
58 # Build up an array of range expressions.
60 "%s-%s" % (six.unichr(low), six.unichr(high))
61 for (low, high) in illegal_chars_u]
64 return re.compile(six.u('[%s]') % six.u('').join(illegal_ranges))
67 def _quote_attribute(text):
68 """Returns the given text in a manner safe for usage in an XML attribute.
70 @param text the text that should appear within an XML attribute.
71 @return the attribute-escaped version of the input text.
73 return xml.sax.saxutils.quoteattr(text)
75 def _replace_invalid_xml(self, str_or_unicode):
76 """Replaces invalid XML characters with a '?'.
78 @param str_or_unicode a string to replace invalid XML
79 characters within. Can be unicode or not. If not unicode,
80 assumes it is a byte string in utf-8 encoding.
82 @returns a utf-8-encoded byte string with invalid
83 XML replaced with '?'.
85 # Get the content into unicode
86 if isinstance(str_or_unicode, str):
87 unicode_content = str_or_unicode.decode('utf-8')
89 unicode_content = str_or_unicode
90 return self.invalid_xml_re.sub(
91 six.u('?'), unicode_content).encode('utf-8')
95 """@return arg parser used to parse formatter-specific options."""
96 parser = super(XunitFormatter, cls).arg_parser()
98 # These are valid choices for results mapping.
99 results_mapping_choices = [
100 XunitFormatter.RM_IGNORE,
101 XunitFormatter.RM_SUCCESS,
102 XunitFormatter.RM_FAILURE,
103 XunitFormatter.RM_PASSTHRU]
105 "--assert-on-unknown-events",
107 help=('cause unknown test events to generate '
108 'a python assert. Default is to ignore.'))
110 "--ignore-skip-name",
114 dest='ignore_skip_name_patterns',
115 help=('a python regex pattern, where '
116 'any skipped test with a test method name where regex '
117 'matches (via search) will be ignored for xUnit test '
118 'result purposes. Can be specified multiple times.'))
120 "--ignore-skip-reason",
124 dest='ignore_skip_reason_patterns',
125 help=('a python regex pattern, where '
126 'any skipped test with a skip reason where the regex '
127 'matches (via search) will be ignored for xUnit test '
128 'result purposes. Can be specified multiple times.'))
130 "--xpass", action="store", choices=results_mapping_choices,
131 default=XunitFormatter.RM_FAILURE,
132 help=('specify mapping from unexpected success to jUnit/xUnit '
135 "--xfail", action="store", choices=results_mapping_choices,
136 default=XunitFormatter.RM_IGNORE,
137 help=('specify mapping from expected failure to jUnit/xUnit '
142 def _build_regex_list_from_patterns(patterns):
143 """Builds a list of compiled regular expressions from option value.
145 @param patterns contains a list of regular expression
148 @return list of compiled regular expressions, empty if no
152 if patterns is not None:
153 for pattern in patterns:
154 regex_list.append(re.compile(pattern))
157 def __init__(self, out_file, options, file_is_stream):
158 """Initializes the XunitFormatter instance.
159 @param out_file file-like object where formatted output is written.
160 @param options specifies a dictionary of options for the
163 # Initialize the parent
164 super(XunitFormatter, self).__init__(out_file, options, file_is_stream)
165 self.text_encoding = "UTF-8"
166 self.invalid_xml_re = XunitFormatter._build_illegal_xml_regex()
167 self.total_test_count = 0
168 self.ignore_skip_name_regexes = (
169 XunitFormatter._build_regex_list_from_patterns(
170 options.ignore_skip_name_patterns))
171 self.ignore_skip_reason_regexes = (
172 XunitFormatter._build_regex_list_from_patterns(
173 options.ignore_skip_reason_patterns))
180 "unexpected_successes": [],
181 "expected_failures": [],
185 self.status_handlers = {
186 EventBuilder.STATUS_SUCCESS: self._handle_success,
187 EventBuilder.STATUS_FAILURE: self._handle_failure,
188 EventBuilder.STATUS_ERROR: self._handle_error,
189 EventBuilder.STATUS_SKIP: self._handle_skip,
190 EventBuilder.STATUS_EXPECTED_FAILURE:
191 self._handle_expected_failure,
192 EventBuilder.STATUS_EXPECTED_TIMEOUT:
193 self._handle_expected_timeout,
194 EventBuilder.STATUS_UNEXPECTED_SUCCESS:
195 self._handle_unexpected_success,
196 EventBuilder.STATUS_EXCEPTIONAL_EXIT:
197 self._handle_exceptional_exit,
198 EventBuilder.STATUS_TIMEOUT:
202 RESULT_TYPES = {EventBuilder.TYPE_TEST_RESULT, EventBuilder.TYPE_JOB_RESULT}
204 def handle_event(self, test_event):
205 super(XunitFormatter, self).handle_event(test_event)
207 event_type = test_event["event"]
208 if event_type is None:
211 if event_type == "terminate":
212 # Process all the final result events into their
214 for result_event in self.result_events.values():
215 self._process_test_result(result_event)
216 self._finish_output()
218 # This is an unknown event.
219 if self.options.assert_on_unknown_events:
220 raise Exception("unknown event type {} from {}\n".format(
221 event_type, test_event))
223 def _handle_success(self, test_event):
224 """Handles a test success.
225 @param test_event the test event to handle.
227 result = self._common_add_testcase_entry(test_event)
229 self.elements["successes"].append(result)
231 def _handle_failure(self, test_event):
232 """Handles a test failure.
233 @param test_event the test event to handle.
235 message = self._replace_invalid_xml(test_event["issue_message"])
236 backtrace = self._replace_invalid_xml(
237 "".join(test_event.get("issue_backtrace", [])))
239 result = self._common_add_testcase_entry(
242 '<failure type={} message={}><![CDATA[{}]]></failure>'.format(
243 XunitFormatter._quote_attribute(test_event["issue_class"]),
244 XunitFormatter._quote_attribute(message),
248 self.elements["failures"].append(result)
250 def _handle_error_build(self, test_event):
251 """Handles a test error.
252 @param test_event the test event to handle.
254 message = self._replace_invalid_xml(test_event["issue_message"])
255 build_issue_description = self._replace_invalid_xml(
256 BuildError.format_build_error(
257 test_event.get("build_command", "<None>"),
258 test_event.get("build_error", "<None>")))
260 result = self._common_add_testcase_entry(
263 '<error type={} message={}><![CDATA[{}]]></error>'.format(
264 XunitFormatter._quote_attribute(test_event["issue_class"]),
265 XunitFormatter._quote_attribute(message),
266 build_issue_description)
269 self.elements["errors"].append(result)
271 def _handle_error_standard(self, test_event):
272 """Handles a test error.
273 @param test_event the test event to handle.
275 message = self._replace_invalid_xml(test_event["issue_message"])
276 backtrace = self._replace_invalid_xml(
277 "".join(test_event.get("issue_backtrace", [])))
279 result = self._common_add_testcase_entry(
282 '<error type={} message={}><![CDATA[{}]]></error>'.format(
283 XunitFormatter._quote_attribute(test_event["issue_class"]),
284 XunitFormatter._quote_attribute(message),
288 self.elements["errors"].append(result)
290 def _handle_error(self, test_event):
291 if test_event.get("issue_phase", None) == "build":
292 self._handle_error_build(test_event)
294 self._handle_error_standard(test_event)
296 def _handle_exceptional_exit(self, test_event):
297 """Handles an exceptional exit.
298 @param test_event the test method or job result event to handle.
300 if "test_name" in test_event:
301 name = test_event["test_name"]
303 name = test_event.get("test_filename", "<unknown test/filename>")
305 message_text = "ERROR: {} ({}): {}".format(
306 test_event.get("exception_code", 0),
307 test_event.get("exception_description", ""),
309 message = self._replace_invalid_xml(message_text)
311 result = self._common_add_testcase_entry(
314 '<error type={} message={}></error>'.format(
316 XunitFormatter._quote_attribute(message))
319 self.elements["errors"].append(result)
321 def _handle_timeout(self, test_event):
322 """Handles a test method or job timeout.
323 @param test_event the test method or job result event to handle.
325 if "test_name" in test_event:
326 name = test_event["test_name"]
328 name = test_event.get("test_filename", "<unknown test/filename>")
330 message_text = "TIMEOUT: {}".format(name)
331 message = self._replace_invalid_xml(message_text)
333 result = self._common_add_testcase_entry(
336 '<error type={} message={}></error>'.format(
338 XunitFormatter._quote_attribute(message))
341 self.elements["errors"].append(result)
344 def _ignore_based_on_regex_list(test_event, test_key, regex_list):
345 """Returns whether to ignore a test event based on patterns.
347 @param test_event the test event dictionary to check.
348 @param test_key the key within the dictionary to check.
349 @param regex_list a list of zero or more regexes. May contain
350 zero or more compiled regexes.
352 @return True if any o the regex list match based on the
353 re.search() method; false otherwise.
355 for regex in regex_list:
356 match = regex.search(test_event.get(test_key, ''))
361 def _handle_skip(self, test_event):
362 """Handles a skipped test.
363 @param test_event the test event to handle.
366 # Are we ignoring this test based on test name?
367 if XunitFormatter._ignore_based_on_regex_list(
368 test_event, 'test_name', self.ignore_skip_name_regexes):
371 # Are we ignoring this test based on skip reason?
372 if XunitFormatter._ignore_based_on_regex_list(
373 test_event, 'skip_reason', self.ignore_skip_reason_regexes):
376 # We're not ignoring this test. Process the skip.
377 reason = self._replace_invalid_xml(test_event.get("skip_reason", ""))
378 result = self._common_add_testcase_entry(
380 inner_content='<skipped message={} />'.format(
381 XunitFormatter._quote_attribute(reason)))
383 self.elements["skips"].append(result)
385 def _handle_expected_failure(self, test_event):
386 """Handles a test that failed as expected.
387 @param test_event the test event to handle.
389 if self.options.xfail == XunitFormatter.RM_PASSTHRU:
390 # This is not a natively-supported junit/xunit
391 # testcase mode, so it might fail a validating
392 # test results viewer.
393 if "bugnumber" in test_event:
394 bug_id_attribute = 'bug-id={} '.format(
395 XunitFormatter._quote_attribute(test_event["bugnumber"]))
397 bug_id_attribute = ''
399 result = self._common_add_testcase_entry(
402 '<expected-failure {}type={} message={} />'.format(
404 XunitFormatter._quote_attribute(
405 test_event["issue_class"]),
406 XunitFormatter._quote_attribute(
407 test_event["issue_message"]))
410 self.elements["expected_failures"].append(result)
411 elif self.options.xfail == XunitFormatter.RM_SUCCESS:
412 result = self._common_add_testcase_entry(test_event)
414 self.elements["successes"].append(result)
415 elif self.options.xfail == XunitFormatter.RM_FAILURE:
416 result = self._common_add_testcase_entry(
418 inner_content='<failure type={} message={} />'.format(
419 XunitFormatter._quote_attribute(test_event["issue_class"]),
420 XunitFormatter._quote_attribute(
421 test_event["issue_message"])))
423 self.elements["failures"].append(result)
424 elif self.options.xfail == XunitFormatter.RM_IGNORE:
428 "unknown xfail option: {}".format(self.options.xfail))
431 def _handle_expected_timeout(test_event):
432 """Handles expected_timeout.
433 @param test_event the test event to handle.
435 # We don't do anything with expected timeouts, not even report.
438 def _handle_unexpected_success(self, test_event):
439 """Handles a test that passed but was expected to fail.
440 @param test_event the test event to handle.
442 if self.options.xpass == XunitFormatter.RM_PASSTHRU:
443 # This is not a natively-supported junit/xunit
444 # testcase mode, so it might fail a validating
445 # test results viewer.
446 result = self._common_add_testcase_entry(
448 inner_content="<unexpected-success />")
450 self.elements["unexpected_successes"].append(result)
451 elif self.options.xpass == XunitFormatter.RM_SUCCESS:
452 # Treat the xpass as a success.
453 result = self._common_add_testcase_entry(test_event)
455 self.elements["successes"].append(result)
456 elif self.options.xpass == XunitFormatter.RM_FAILURE:
457 # Treat the xpass as a failure.
458 if "bugnumber" in test_event:
459 message = "unexpected success (bug_id:{})".format(
460 test_event["bugnumber"])
462 message = "unexpected success (bug_id:none)"
463 result = self._common_add_testcase_entry(
465 inner_content='<failure type={} message={} />'.format(
466 XunitFormatter._quote_attribute("unexpected_success"),
467 XunitFormatter._quote_attribute(message)))
469 self.elements["failures"].append(result)
470 elif self.options.xpass == XunitFormatter.RM_IGNORE:
471 # Ignore the xpass result as far as xUnit reporting goes.
474 raise Exception("unknown xpass option: {}".format(
477 def _process_test_result(self, test_event):
478 """Processes the test_event known to be a test result.
480 This categorizes the event appropriately and stores the data needed
481 to generate the final xUnit report. This method skips events that
482 cannot be represented in xUnit output.
484 if "status" not in test_event:
485 raise Exception("test event dictionary missing 'status' key")
487 status = test_event["status"]
488 if status not in self.status_handlers:
489 raise Exception("test event status '{}' unsupported".format(
492 # Call the status handler for the test result.
493 self.status_handlers[status](test_event)
495 def _common_add_testcase_entry(self, test_event, inner_content=None):
496 """Registers a testcase result, and returns the text created.
498 The caller is expected to manage failure/skip/success counts
499 in some kind of appropriate way. This call simply constructs
500 the XML and appends the returned result to the self.all_results
503 @param test_event the test event dictionary.
505 @param inner_content if specified, gets included in the <testcase>
506 inner section, at the point before stdout and stderr would be
507 included. This is where a <failure/>, <skipped/>, <error/>, etc.
510 @return the text of the xml testcase element.
514 test_class = test_event.get("test_class", "<no_class>")
515 test_name = test_event.get("test_name", "<no_test_method>")
516 event_time = test_event["event_time"]
517 time_taken = self.elapsed_time_for_test(
518 test_class, test_name, event_time)
520 # Plumb in stdout/stderr once we shift over to only test results.
524 # Formulate the output xml.
525 if not inner_content:
528 '<testcase classname="{}" name="{}" time="{:.3f}">'
529 '{}{}{}</testcase>'.format(
537 # Save the result, update total test count.
539 self.total_test_count += 1
540 self.elements["all"].append(result)
544 def _finish_output_no_lock(self):
545 """Flushes out the report of test executions to form valid xml output.
547 xUnit output is in XML. The reporting system cannot complete the
548 formatting of the output without knowing when there is no more input.
549 This call addresses notification of the completed test run and thus is
550 when we can finish off the report output.
553 # Figure out the counts line for the testsuite. If we have
554 # been counting either unexpected successes or expected
555 # failures, we'll output those in the counts, at the risk of
556 # being invalidated by a validating test results viewer.
557 # These aren't counted by default so they won't show up unless
558 # the user specified a formatter option to include them.
559 xfail_count = len(self.elements["expected_failures"])
560 xpass_count = len(self.elements["unexpected_successes"])
561 if xfail_count > 0 or xpass_count > 0:
562 extra_testsuite_attributes = (
563 ' expected-failures="{}"'
564 ' unexpected-successes="{}"'.format(xfail_count, xpass_count))
566 extra_testsuite_attributes = ""
570 '<?xml version="1.0" encoding="{}"?>\n'
572 '<testsuite name="{}" tests="{}" errors="{}" failures="{}" '
573 'skip="{}"{}>\n'.format(
576 self.total_test_count,
577 len(self.elements["errors"]),
578 len(self.elements["failures"]),
579 len(self.elements["skips"]),
580 extra_testsuite_attributes))
582 # Output each of the test result entries.
583 for result in self.elements["all"]:
584 self.out_file.write(result + '\n')
586 # Close off the test suite.
587 self.out_file.write('</testsuite></testsuites>\n')
589 def _finish_output(self):
590 """Finish writing output as all incoming events have arrived."""
592 self._finish_output_no_lock()