]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - packages/Python/lldbsuite/test_event/formatter/xunit.py
Vendor import of lldb release_39 branch r276489:
[FreeBSD/FreeBSD.git] / packages / Python / lldbsuite / test_event / formatter / xunit.py
1 """
2                      The LLVM Compiler Infrastructure
3
4 This file is distributed under the University of Illinois Open Source
5 License. See LICENSE.TXT for details.
6
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.
10 """
11 from __future__ import absolute_import
12 from __future__ import print_function
13
14 # System modules
15 import re
16 import sys
17 import xml.sax.saxutils
18
19 # Third-party modules
20 import six
21
22 # Local modules
23 from ..event_builder import EventBuilder
24 from ..build_exception import BuildError
25 from .results_formatter import ResultsFormatter
26
27
28 class XunitFormatter(ResultsFormatter):
29     """Provides xUnit-style formatted output.
30     """
31
32     # Result mapping arguments
33     RM_IGNORE = 'ignore'
34     RM_SUCCESS = 'success'
35     RM_FAILURE = 'failure'
36     RM_PASSTHRU = 'passthru'
37
38     @staticmethod
39     def _build_illegal_xml_regex():
40         """Constructs a regex to match all illegal xml characters.
41
42         Expects to be used against a unicode string."""
43         # Construct the range pairs of invalid unicode characters.
44         illegal_chars_u = [
45             (0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84),
46             (0x86, 0x9F), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)]
47
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)])
57
58         # Build up an array of range expressions.
59         illegal_ranges = [
60             "%s-%s" % (six.unichr(low), six.unichr(high))
61             for (low, high) in illegal_chars_u]
62
63         # Compile the regex
64         return re.compile(six.u('[%s]') % six.u('').join(illegal_ranges))
65
66     @staticmethod
67     def _quote_attribute(text):
68         """Returns the given text in a manner safe for usage in an XML attribute.
69
70         @param text the text that should appear within an XML attribute.
71         @return the attribute-escaped version of the input text.
72         """
73         return xml.sax.saxutils.quoteattr(text)
74
75     def _replace_invalid_xml(self, str_or_unicode):
76         """Replaces invalid XML characters with a '?'.
77
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.
81
82         @returns a utf-8-encoded byte string with invalid
83         XML replaced with '?'.
84         """
85         # Get the content into unicode
86         if isinstance(str_or_unicode, str):
87             unicode_content = str_or_unicode.decode('utf-8')
88         else:
89             unicode_content = str_or_unicode
90         return self.invalid_xml_re.sub(
91             six.u('?'), unicode_content).encode('utf-8')
92
93     @classmethod
94     def arg_parser(cls):
95         """@return arg parser used to parse formatter-specific options."""
96         parser = super(XunitFormatter, cls).arg_parser()
97
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]
104         parser.add_argument(
105             "--assert-on-unknown-events",
106             action="store_true",
107             help=('cause unknown test events to generate '
108                   'a python assert.  Default is to ignore.'))
109         parser.add_argument(
110             "--ignore-skip-name",
111             "-n",
112             metavar='PATTERN',
113             action="append",
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.'))
119         parser.add_argument(
120             "--ignore-skip-reason",
121             "-r",
122             metavar='PATTERN',
123             action="append",
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.'))
129         parser.add_argument(
130             "--xpass", action="store", choices=results_mapping_choices,
131             default=XunitFormatter.RM_FAILURE,
132             help=('specify mapping from unexpected success to jUnit/xUnit '
133                   'result type'))
134         parser.add_argument(
135             "--xfail", action="store", choices=results_mapping_choices,
136             default=XunitFormatter.RM_IGNORE,
137             help=('specify mapping from expected failure to jUnit/xUnit '
138                   'result type'))
139         return parser
140
141     @staticmethod
142     def _build_regex_list_from_patterns(patterns):
143         """Builds a list of compiled regular expressions from option value.
144
145         @param patterns contains a list of regular expression
146         patterns.
147
148         @return list of compiled regular expressions, empty if no
149         patterns provided.
150         """
151         regex_list = []
152         if patterns is not None:
153             for pattern in patterns:
154                 regex_list.append(re.compile(pattern))
155         return regex_list
156
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
161         formatter.
162         """
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))
174
175         self.elements = {
176             "successes": [],
177             "errors": [],
178             "failures": [],
179             "skips": [],
180             "unexpected_successes": [],
181             "expected_failures": [],
182             "all": []
183             }
184
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:
199                 self._handle_timeout
200             }
201
202     RESULT_TYPES = {EventBuilder.TYPE_TEST_RESULT, EventBuilder.TYPE_JOB_RESULT}
203
204     def handle_event(self, test_event):
205         super(XunitFormatter, self).handle_event(test_event)
206
207         event_type = test_event["event"]
208         if event_type is None:
209             return
210
211         if event_type == "terminate":
212             # Process all the final result events into their
213             # XML counterparts.
214             for result_event in self.result_events.values():
215                 self._process_test_result(result_event)
216             self._finish_output()
217         else:
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))
222
223     def _handle_success(self, test_event):
224         """Handles a test success.
225         @param test_event the test event to handle.
226         """
227         result = self._common_add_testcase_entry(test_event)
228         with self.lock:
229             self.elements["successes"].append(result)
230
231     def _handle_failure(self, test_event):
232         """Handles a test failure.
233         @param test_event the test event to handle.
234         """
235         message = self._replace_invalid_xml(test_event["issue_message"])
236         backtrace = self._replace_invalid_xml(
237             "".join(test_event.get("issue_backtrace", [])))
238
239         result = self._common_add_testcase_entry(
240             test_event,
241             inner_content=(
242                 '<failure type={} message={}><![CDATA[{}]]></failure>'.format(
243                     XunitFormatter._quote_attribute(test_event["issue_class"]),
244                     XunitFormatter._quote_attribute(message),
245                     backtrace)
246             ))
247         with self.lock:
248             self.elements["failures"].append(result)
249
250     def _handle_error_build(self, test_event):
251         """Handles a test error.
252         @param test_event the test event to handle.
253         """
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>")))
259
260         result = self._common_add_testcase_entry(
261             test_event,
262             inner_content=(
263                 '<error type={} message={}><![CDATA[{}]]></error>'.format(
264                     XunitFormatter._quote_attribute(test_event["issue_class"]),
265                     XunitFormatter._quote_attribute(message),
266                     build_issue_description)
267             ))
268         with self.lock:
269             self.elements["errors"].append(result)
270
271     def _handle_error_standard(self, test_event):
272         """Handles a test error.
273         @param test_event the test event to handle.
274         """
275         message = self._replace_invalid_xml(test_event["issue_message"])
276         backtrace = self._replace_invalid_xml(
277             "".join(test_event.get("issue_backtrace", [])))
278
279         result = self._common_add_testcase_entry(
280             test_event,
281             inner_content=(
282                 '<error type={} message={}><![CDATA[{}]]></error>'.format(
283                     XunitFormatter._quote_attribute(test_event["issue_class"]),
284                     XunitFormatter._quote_attribute(message),
285                     backtrace)
286             ))
287         with self.lock:
288             self.elements["errors"].append(result)
289
290     def _handle_error(self, test_event):
291         if test_event.get("issue_phase", None) == "build":
292             self._handle_error_build(test_event)
293         else:
294             self._handle_error_standard(test_event)
295
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.
299         """
300         if "test_name" in test_event:
301             name = test_event["test_name"]
302         else:
303             name = test_event.get("test_filename", "<unknown test/filename>")
304
305         message_text = "ERROR: {} ({}): {}".format(
306             test_event.get("exception_code", 0),
307             test_event.get("exception_description", ""),
308             name)
309         message = self._replace_invalid_xml(message_text)
310
311         result = self._common_add_testcase_entry(
312             test_event,
313             inner_content=(
314                 '<error type={} message={}></error>'.format(
315                     "exceptional_exit",
316                     XunitFormatter._quote_attribute(message))
317             ))
318         with self.lock:
319             self.elements["errors"].append(result)
320
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.
324         """
325         if "test_name" in test_event:
326             name = test_event["test_name"]
327         else:
328             name = test_event.get("test_filename", "<unknown test/filename>")
329
330         message_text = "TIMEOUT: {}".format(name)
331         message = self._replace_invalid_xml(message_text)
332
333         result = self._common_add_testcase_entry(
334             test_event,
335             inner_content=(
336                 '<error type={} message={}></error>'.format(
337                     "timeout",
338                     XunitFormatter._quote_attribute(message))
339             ))
340         with self.lock:
341             self.elements["errors"].append(result)
342
343     @staticmethod
344     def _ignore_based_on_regex_list(test_event, test_key, regex_list):
345         """Returns whether to ignore a test event based on patterns.
346
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.
351
352         @return True if any o the regex list match based on the
353         re.search() method; false otherwise.
354         """
355         for regex in regex_list:
356             match = regex.search(test_event.get(test_key, ''))
357             if match:
358                 return True
359         return False
360
361     def _handle_skip(self, test_event):
362         """Handles a skipped test.
363         @param test_event the test event to handle.
364         """
365
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):
369             return
370
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):
374             return
375
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(
379             test_event,
380             inner_content='<skipped message={} />'.format(
381                 XunitFormatter._quote_attribute(reason)))
382         with self.lock:
383             self.elements["skips"].append(result)
384
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.
388         """
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"]))
396             else:
397                 bug_id_attribute = ''
398
399             result = self._common_add_testcase_entry(
400                 test_event,
401                 inner_content=(
402                     '<expected-failure {}type={} message={} />'.format(
403                         bug_id_attribute,
404                         XunitFormatter._quote_attribute(
405                             test_event["issue_class"]),
406                         XunitFormatter._quote_attribute(
407                             test_event["issue_message"]))
408                 ))
409             with self.lock:
410                 self.elements["expected_failures"].append(result)
411         elif self.options.xfail == XunitFormatter.RM_SUCCESS:
412             result = self._common_add_testcase_entry(test_event)
413             with self.lock:
414                 self.elements["successes"].append(result)
415         elif self.options.xfail == XunitFormatter.RM_FAILURE:
416             result = self._common_add_testcase_entry(
417                 test_event,
418                 inner_content='<failure type={} message={} />'.format(
419                     XunitFormatter._quote_attribute(test_event["issue_class"]),
420                     XunitFormatter._quote_attribute(
421                         test_event["issue_message"])))
422             with self.lock:
423                 self.elements["failures"].append(result)
424         elif self.options.xfail == XunitFormatter.RM_IGNORE:
425             pass
426         else:
427             raise Exception(
428                 "unknown xfail option: {}".format(self.options.xfail))
429
430     @staticmethod
431     def _handle_expected_timeout(test_event):
432         """Handles expected_timeout.
433         @param test_event the test event to handle.
434         """
435         # We don't do anything with expected timeouts, not even report.
436         pass
437
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.
441         """
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(
447                 test_event,
448                 inner_content="<unexpected-success />")
449             with self.lock:
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)
454             with self.lock:
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"])
461             else:
462                 message = "unexpected success (bug_id:none)"
463             result = self._common_add_testcase_entry(
464                 test_event,
465                 inner_content='<failure type={} message={} />'.format(
466                     XunitFormatter._quote_attribute("unexpected_success"),
467                     XunitFormatter._quote_attribute(message)))
468             with self.lock:
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.
472             pass
473         else:
474             raise Exception("unknown xpass option: {}".format(
475                 self.options.xpass))
476
477     def _process_test_result(self, test_event):
478         """Processes the test_event known to be a test result.
479
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.
483         """
484         if "status" not in test_event:
485             raise Exception("test event dictionary missing 'status' key")
486
487         status = test_event["status"]
488         if status not in self.status_handlers:
489             raise Exception("test event status '{}' unsupported".format(
490                 status))
491
492         # Call the status handler for the test result.
493         self.status_handlers[status](test_event)
494
495     def _common_add_testcase_entry(self, test_event, inner_content=None):
496         """Registers a testcase result, and returns the text created.
497
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
501         list.
502
503         @param test_event the test event dictionary.
504
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.
508         could go.
509
510         @return the text of the xml testcase element.
511         """
512
513         # Get elapsed time.
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)
519
520         # Plumb in stdout/stderr once we shift over to only test results.
521         test_stdout = ''
522         test_stderr = ''
523
524         # Formulate the output xml.
525         if not inner_content:
526             inner_content = ""
527         result = (
528             '<testcase classname="{}" name="{}" time="{:.3f}">'
529             '{}{}{}</testcase>'.format(
530                 test_class,
531                 test_name,
532                 time_taken,
533                 inner_content,
534                 test_stdout,
535                 test_stderr))
536
537         # Save the result, update total test count.
538         with self.lock:
539             self.total_test_count += 1
540             self.elements["all"].append(result)
541
542         return result
543
544     def _finish_output_no_lock(self):
545         """Flushes out the report of test executions to form valid xml output.
546
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.
551         """
552
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))
565         else:
566             extra_testsuite_attributes = ""
567
568         # Output the header.
569         self.out_file.write(
570             '<?xml version="1.0" encoding="{}"?>\n'
571             '<testsuites>'
572             '<testsuite name="{}" tests="{}" errors="{}" failures="{}" '
573             'skip="{}"{}>\n'.format(
574                 self.text_encoding,
575                 "LLDB test suite",
576                 self.total_test_count,
577                 len(self.elements["errors"]),
578                 len(self.elements["failures"]),
579                 len(self.elements["skips"]),
580                 extra_testsuite_attributes))
581
582         # Output each of the test result entries.
583         for result in self.elements["all"]:
584             self.out_file.write(result + '\n')
585
586         # Close off the test suite.
587         self.out_file.write('</testsuite></testsuites>\n')
588
589     def _finish_output(self):
590         """Finish writing output as all incoming events have arrived."""
591         with self.lock:
592             self._finish_output_no_lock()