blob: 454ee016663342a0e55ba03f67a753db45be321d [file] [log] [blame]
Todd Fiala68615ce2015-09-15 21:38:04 +00001"""
2 The LLVM Compiler Infrastructure
3
4This file is distributed under the University of Illinois Open Source
5License. See LICENSE.TXT for details.
6
7Provides classes used by the test results reporting infrastructure
8within the LLDB test suite.
9"""
10
11import argparse
12import cPickle
13import inspect
14import os
15import sys
16import threading
17import time
18import xml.sax.saxutils
19
20
21class EventBuilder(object):
22 """Helper class to build test result event dictionaries."""
23 @staticmethod
24 def _get_test_name_info(test):
25 """Returns (test-class-name, test-method-name) from a test case instance.
26
27 @param test a unittest.TestCase instance.
28
29 @return tuple containing (test class name, test method name)
30 """
31 test_class_components = test.id().split(".")
32 test_class_name = ".".join(test_class_components[:-1])
33 test_name = test_class_components[-1]
34 return (test_class_name, test_name)
35
36 @staticmethod
37 def _event_dictionary_common(test, event_type):
38 """Returns an event dictionary setup with values for the given event type.
39
40 @param test the unittest.TestCase instance
41
42 @param event_type the name of the event type (string).
43
44 @return event dictionary with common event fields set.
45 """
46 test_class_name, test_name = EventBuilder._get_test_name_info(test)
47 return {
48 "event": event_type,
49 "test_class": test_class_name,
50 "test_name": test_name,
51 "event_time": time.time()
52 }
53
54 @staticmethod
55 def _error_tuple_class(error_tuple):
56 """Returns the unittest error tuple's error class as a string.
57
58 @param error_tuple the error tuple provided by the test framework.
59
60 @return the error type (typically an exception) raised by the
61 test framework.
62 """
63 type_var = error_tuple[0]
64 module = inspect.getmodule(type_var)
65 if module:
66 return "{}.{}".format(module.__name__, type_var.__name__)
67 else:
68 return type_var.__name__
69
70 @staticmethod
71 def _error_tuple_message(error_tuple):
72 """Returns the unittest error tuple's error message.
73
74 @param error_tuple the error tuple provided by the test framework.
75
76 @return the error message provided by the test framework.
77 """
78 return str(error_tuple[1])
79
80 @staticmethod
81 def _event_dictionary_test_result(test, status):
82 """Returns an event dictionary with common test result fields set.
83
84 @param test a unittest.TestCase instance.
85
86 @param status the status/result of the test
87 (e.g. "success", "failure", etc.)
88
89 @return the event dictionary
90 """
91 event = EventBuilder._event_dictionary_common(test, "test_result")
92 event["status"] = status
93 return event
94
95 @staticmethod
96 def _event_dictionary_issue(test, status, error_tuple):
97 """Returns an event dictionary with common issue-containing test result
98 fields set.
99
100 @param test a unittest.TestCase instance.
101
102 @param status the status/result of the test
103 (e.g. "success", "failure", etc.)
104
105 @param error_tuple the error tuple as reported by the test runner.
106 This is of the form (type<error>, error).
107
108 @return the event dictionary
109 """
110 event = EventBuilder._event_dictionary_test_result(test, status)
111 event["issue_class"] = EventBuilder._error_tuple_class(error_tuple)
112 event["issue_message"] = EventBuilder._error_tuple_message(error_tuple)
113 return event
114
115 @staticmethod
116 def event_for_start(test):
117 """Returns an event dictionary for the test start event.
118
119 @param test a unittest.TestCase instance.
120
121 @return the event dictionary
122 """
123 return EventBuilder._event_dictionary_common(test, "test_start")
124
125 @staticmethod
126 def event_for_success(test):
127 """Returns an event dictionary for a successful test.
128
129 @param test a unittest.TestCase instance.
130
131 @return the event dictionary
132 """
133 return EventBuilder._event_dictionary_test_result(test, "success")
134
135 @staticmethod
136 def event_for_unexpected_success(test, bugnumber):
137 """Returns an event dictionary for a test that succeeded but was
138 expected to fail.
139
140 @param test a unittest.TestCase instance.
141
142 @param bugnumber the issue identifier for the bug tracking the
143 fix request for the test expected to fail (but is in fact
144 passing here).
145
146 @return the event dictionary
147
148 """
149 event = EventBuilder._event_dictionary_test_result(
150 test, "unexpected_success")
151 if bugnumber:
152 event["bugnumber"] = str(bugnumber)
153 return event
154
155 @staticmethod
156 def event_for_failure(test, error_tuple):
157 """Returns an event dictionary for a test that failed.
158
159 @param test a unittest.TestCase instance.
160
161 @param error_tuple the error tuple as reported by the test runner.
162 This is of the form (type<error>, error).
163
164 @return the event dictionary
165 """
166 return EventBuilder._event_dictionary_issue(
167 test, "failure", error_tuple)
168
169 @staticmethod
170 def event_for_expected_failure(test, error_tuple, bugnumber):
171 """Returns an event dictionary for a test that failed as expected.
172
173 @param test a unittest.TestCase instance.
174
175 @param error_tuple the error tuple as reported by the test runner.
176 This is of the form (type<error>, error).
177
178 @param bugnumber the issue identifier for the bug tracking the
179 fix request for the test expected to fail.
180
181 @return the event dictionary
182
183 """
184 event = EventBuilder._event_dictionary_issue(
185 test, "expected_failure", error_tuple)
186 if bugnumber:
187 event["bugnumber"] = str(bugnumber)
188 return event
189
190 @staticmethod
191 def event_for_skip(test, reason):
192 """Returns an event dictionary for a test that was skipped.
193
194 @param test a unittest.TestCase instance.
195
196 @param reason the reason why the test is being skipped.
197
198 @return the event dictionary
199 """
200 event = EventBuilder._event_dictionary_test_result(test, "skip")
201 event["skip_reason"] = reason
202 return event
203
204 @staticmethod
205 def event_for_error(test, error_tuple):
206 """Returns an event dictionary for a test that hit a test execution error.
207
208 @param test a unittest.TestCase instance.
209
210 @param error_tuple the error tuple as reported by the test runner.
211 This is of the form (type<error>, error).
212
213 @return the event dictionary
214 """
215 return EventBuilder._event_dictionary_issue(test, "error", error_tuple)
216
217 @staticmethod
218 def event_for_cleanup_error(test, error_tuple):
219 """Returns an event dictionary for a test that hit a test execution error
220 during the test cleanup phase.
221
222 @param test a unittest.TestCase instance.
223
224 @param error_tuple the error tuple as reported by the test runner.
225 This is of the form (type<error>, error).
226
227 @return the event dictionary
228 """
229 event = EventBuilder._event_dictionary_issue(
230 test, "error", error_tuple)
231 event["issue_phase"] = "cleanup"
232 return event
233
234
235class ResultsFormatter(object):
236 """Provides interface to formatting test results out to a file-like object.
237
238 This class allows the LLDB test framework's raw test-realted
239 events to be processed and formatted in any manner desired.
240 Test events are represented by python dictionaries, formatted
241 as in the EventBuilder class above.
242
243 ResultFormatter instances are given a file-like object in which
244 to write their results.
245
246 ResultFormatter lifetime looks like the following:
247
248 # The result formatter is created.
249 # The argparse options dictionary is generated from calling
250 # the SomeResultFormatter.arg_parser() with the options data
251 # passed to dotest.py via the "--results-formatter-options"
252 # argument. See the help on that for syntactic requirements
253 # on getting that parsed correctly.
254 formatter = SomeResultFormatter(file_like_object, argpared_options_dict)
255
256 # Single call to session start, before parsing any events.
257 formatter.begin_session()
258
259 # Zero or more calls specified for events recorded during the test session.
260 # The parallel test runner manages getting results from all the inferior
261 # dotest processes, so from a new format perspective, don't worry about
262 # that. The formatter will be presented with a single stream of events
263 # sandwiched between a single begin_session()/end_session() pair in the
264 # parallel test runner process/thread.
265 for event in zero_or_more_test_events():
266 formatter.process_event(event)
267
268 # Single call to session end. Formatters that need all the data before
269 # they can print a correct result (e.g. xUnit/JUnit), this is where
270 # the final report can be generated.
271 formatter.end_session()
272
273 It is not the formatter's responsibility to close the file_like_object.
274 (i.e. do not close it).
275
276 The lldb test framework passes these test events in real time, so they
277 arrive as they come in.
278
279 In the case of the parallel test runner, the dotest inferiors
280 add a 'pid' field to the dictionary that indicates which inferior
281 pid generated the event.
282
283 Note more events may be added in the future to support richer test
284 reporting functionality. One example: creating a true flaky test
285 result category so that unexpected successes really mean the test
286 is marked incorrectly (either should be marked flaky, or is indeed
287 passing consistently now and should have the xfail marker
288 removed). In this case, a flaky_success and flaky_fail event
289 likely will be added to capture these and support reporting things
290 like percentages of flaky test passing so we can see if we're
291 making some things worse/better with regards to failure rates.
292
293 Another example: announcing all the test methods that are planned
294 to be run, so we can better support redo operations of various kinds
295 (redo all non-run tests, redo non-run tests except the one that
296 was running [perhaps crashed], etc.)
297
298 Implementers are expected to override all the public methods
299 provided in this class. See each method's docstring to see
300 expectations about when the call should be chained.
301
302 """
303
304 @classmethod
305 def arg_parser(cls):
306 """@return arg parser used to parse formatter-specific options."""
307 parser = argparse.ArgumentParser(
308 description='{} options'.format(cls.__name__),
309 usage=('dotest.py --results-formatter-options='
310 '"--option1 value1 [--option2 value2 [...]]"'))
311 return parser
312
313 def __init__(self, out_file, options):
314 super(ResultsFormatter, self).__init__()
315 self.out_file = out_file
316 self.options = options
317 if not self.out_file:
318 raise Exception("ResultsFormatter created with no file object")
319 self.start_time_by_test = {}
320
321 # Lock that we use while mutating inner state, like the
322 # total test count and the elements. We minimize how
323 # long we hold the lock just to keep inner state safe, not
324 # entirely consistent from the outside.
325 self.lock = threading.Lock()
326
327 def begin_session(self):
328 """Begins a test session.
329
330 All process_event() calls must be sandwiched between
331 begin_session() and end_session() calls.
332
333 Derived classes may override this but should call this first.
334 """
335 pass
336
337 def end_session(self):
338 """Ends a test session.
339
340 All process_event() calls must be sandwiched between
341 begin_session() and end_session() calls.
342
343 All results formatting should be sent to the output
344 file object by the end of this call.
345
346 Derived classes may override this but should call this after
347 the dervied class's behavior is complete.
348 """
349 pass
350
351 def process_event(self, test_event):
352 """Processes the test event for collection into the formatter output.
353
354 Derived classes may override this but should call down to this
355 implementation first.
356
357 @param test_event the test event as formatted by one of the
358 event_for_* calls.
359 """
360 pass
361
362 def track_start_time(self, test_class, test_name, start_time):
363 """Tracks the start time of a test so elapsed time can be computed.
364
365 This alleviates the need for test results to be processed serially
366 by test. It will save the start time for the test so that
367 elapsed_time_for_test() can compute the elapsed time properly.
368 """
369 if test_class is None or test_name is None:
370 return
371
372 test_key = "{}.{}".format(test_class, test_name)
373 with self.lock:
374 self.start_time_by_test[test_key] = start_time
375
376 def elapsed_time_for_test(self, test_class, test_name, end_time):
377 """Returns the elapsed time for a test.
378
379 This function can only be called once per test and requires that
380 the track_start_time() method be called sometime prior to calling
381 this method.
382 """
383 if test_class is None or test_name is None:
384 return -2.0
385
386 test_key = "{}.{}".format(test_class, test_name)
387 with self.lock:
388 if test_key not in self.start_time_by_test:
389 return -1.0
390 else:
391 start_time = self.start_time_by_test[test_key]
392 del self.start_time_by_test[test_key]
393 return end_time - start_time
394
395
396class XunitFormatter(ResultsFormatter):
397 """Provides xUnit-style formatted output.
398 """
399
400 # Result mapping arguments
401 RM_IGNORE = 'ignore'
402 RM_SUCCESS = 'success'
403 RM_FAILURE = 'failure'
404 RM_PASSTHRU = 'passthru'
405
406 @staticmethod
407 def _quote_attribute(text):
408 """Returns the given text in a manner safe for usage in an XML attribute.
409
410 @param text the text that should appear within an XML attribute.
411 @return the attribute-escaped version of the input text.
412 """
413 return xml.sax.saxutils.quoteattr(text)
414
415 @classmethod
416 def arg_parser(cls):
417 """@return arg parser used to parse formatter-specific options."""
418 parser = super(XunitFormatter, cls).arg_parser()
419
420 # These are valid choices for results mapping.
421 results_mapping_choices = [
422 XunitFormatter.RM_IGNORE,
423 XunitFormatter.RM_SUCCESS,
424 XunitFormatter.RM_FAILURE,
425 XunitFormatter.RM_PASSTHRU]
426 parser.add_argument(
427 "--xpass", action="store", choices=results_mapping_choices,
428 default=XunitFormatter.RM_FAILURE,
429 help=('specify mapping from unexpected success to jUnit/xUnit '
430 'result type'))
431 parser.add_argument(
432 "--xfail", action="store", choices=results_mapping_choices,
433 default=XunitFormatter.RM_IGNORE,
434 help=('specify mapping from expected failure to jUnit/xUnit '
435 'result type'))
436 return parser
437
438 def __init__(self, out_file, options):
439 """Initializes the XunitFormatter instance.
440 @param out_file file-like object where formatted output is written.
441 @param options_dict specifies a dictionary of options for the
442 formatter.
443 """
444 # Initialize the parent
445 super(XunitFormatter, self).__init__(out_file, options)
446 self.text_encoding = "UTF-8"
447
448 self.total_test_count = 0
449
450 self.elements = {
451 "successes": [],
452 "errors": [],
453 "failures": [],
454 "skips": [],
455 "unexpected_successes": [],
456 "expected_failures": [],
457 "all": []
458 }
459
460 self.status_handlers = {
461 "success": self._handle_success,
462 "failure": self._handle_failure,
463 "error": self._handle_error,
464 "skip": self._handle_skip,
465 "expected_failure": self._handle_expected_failure,
466 "unexpected_success": self._handle_unexpected_success
467 }
468
469 def begin_session(self):
470 super(XunitFormatter, self).begin_session()
471
472 def process_event(self, test_event):
473 super(XunitFormatter, self).process_event(test_event)
474
475 event_type = test_event["event"]
476 if event_type is None:
477 return
478
479 if event_type == "test_start":
480 self.track_start_time(
481 test_event["test_class"],
482 test_event["test_name"],
483 test_event["event_time"])
484 elif event_type == "test_result":
485 self._process_test_result(test_event)
486 else:
487 sys.stderr.write("unknown event type {} from {}\n".format(
488 event_type, test_event))
489
490 def _handle_success(self, test_event):
491 """Handles a test success.
492 @param test_event the test event to handle.
493 """
494 result = self._common_add_testcase_entry(test_event)
495 with self.lock:
496 self.elements["successes"].append(result)
497
498 def _handle_failure(self, test_event):
499 """Handles a test failure.
500 @param test_event the test event to handle.
501 """
502 result = self._common_add_testcase_entry(
503 test_event,
504 inner_content='<failure type={} message={} />'.format(
505 XunitFormatter._quote_attribute(test_event["issue_class"]),
506 XunitFormatter._quote_attribute(test_event["issue_message"])))
507 with self.lock:
508 self.elements["failures"].append(result)
509
510 def _handle_error(self, test_event):
511 """Handles a test error.
512 @param test_event the test event to handle.
513 """
514 result = self._common_add_testcase_entry(
515 test_event,
516 inner_content='<error type={} message={} />'.format(
517 XunitFormatter._quote_attribute(test_event["issue_class"]),
518 XunitFormatter._quote_attribute(test_event["issue_message"])))
519 with self.lock:
520 self.elements["errors"].append(result)
521
522 def _handle_skip(self, test_event):
523 """Handles a skipped test.
524 @param test_event the test event to handle.
525 """
526 result = self._common_add_testcase_entry(
527 test_event,
528 inner_content='<skipped message={} />'.format(
529 XunitFormatter._quote_attribute(test_event["skip_reason"])))
530 with self.lock:
531 self.elements["skips"].append(result)
532
533 def _handle_expected_failure(self, test_event):
534 """Handles a test that failed as expected.
535 @param test_event the test event to handle.
536 """
537 if self.options.xfail == XunitFormatter.RM_PASSTHRU:
538 # This is not a natively-supported junit/xunit
539 # testcase mode, so it might fail a validating
540 # test results viewer.
541 if "bugnumber" in test_event:
542 bug_id_attribute = 'bug-id={} '.format(
543 XunitFormatter._quote_attribute(test_event["bugnumber"]))
544 else:
545 bug_id_attribute = ''
546
547 result = self._common_add_testcase_entry(
548 test_event,
549 inner_content=(
550 '<expected-failure {}type={} message={} />'.format(
551 bug_id_attribute,
552 XunitFormatter._quote_attribute(
553 test_event["issue_class"]),
554 XunitFormatter._quote_attribute(
555 test_event["issue_message"]))
556 ))
557 with self.lock:
558 self.elements["expected_failures"].append(result)
559 elif self.options.xfail == XunitFormatter.RM_SUCCESS:
560 result = self._common_add_testcase_entry(test_event)
561 with self.lock:
562 self.elements["successes"].append(result)
563 elif self.options.xfail == XunitFormatter.RM_FAILURE:
564 result = self._common_add_testcase_entry(
565 test_event,
566 inner_content='<failure type={} message={} />'.format(
567 XunitFormatter._quote_attribute(test_event["issue_class"]),
568 XunitFormatter._quote_attribute(
569 test_event["issue_message"])))
570 with self.lock:
571 self.elements["failures"].append(result)
572 elif self.options.xfail == XunitFormatter.RM_IGNORE:
573 pass
574 else:
575 raise Exception(
576 "unknown xfail option: {}".format(self.options.xfail))
577
578 def _handle_unexpected_success(self, test_event):
579 """Handles a test that passed but was expected to fail.
580 @param test_event the test event to handle.
581 """
582 if self.options.xpass == XunitFormatter.RM_PASSTHRU:
583 # This is not a natively-supported junit/xunit
584 # testcase mode, so it might fail a validating
585 # test results viewer.
586 result = self._common_add_testcase_entry(
587 test_event,
588 inner_content=("<unexpected-success />"))
589 with self.lock:
590 self.elements["unexpected_successes"].append(result)
591 elif self.options.xpass == XunitFormatter.RM_SUCCESS:
592 # Treat the xpass as a success.
593 result = self._common_add_testcase_entry(test_event)
594 with self.lock:
595 self.elements["successes"].append(result)
596 elif self.options.xpass == XunitFormatter.RM_FAILURE:
597 # Treat the xpass as a failure.
598 if "bugnumber" in test_event:
599 message = "unexpected success (bug_id:{})".format(
600 test_event["bugnumber"])
601 else:
602 message = "unexpected success (bug_id:none)"
603 result = self._common_add_testcase_entry(
604 test_event,
605 inner_content='<failure type={} message={} />'.format(
606 XunitFormatter._quote_attribute("unexpected_success"),
607 XunitFormatter._quote_attribute(message)))
608 with self.lock:
609 self.elements["failures"].append(result)
610 elif self.options.xpass == XunitFormatter.RM_IGNORE:
611 # Ignore the xpass result as far as xUnit reporting goes.
612 pass
613 else:
614 raise Exception("unknown xpass option: {}".format(
615 self.options.xpass))
616
617 def _process_test_result(self, test_event):
618 """Processes the test_event known to be a test result.
619
620 This categorizes the event appropriately and stores the data needed
621 to generate the final xUnit report. This method skips events that
622 cannot be represented in xUnit output.
623 """
624 if "status" not in test_event:
625 raise Exception("test event dictionary missing 'status' key")
626
627 status = test_event["status"]
628 if status not in self.status_handlers:
629 raise Exception("test event status '{}' unsupported".format(
630 status))
631
632 # Call the status handler for the test result.
633 self.status_handlers[status](test_event)
634
635 def _common_add_testcase_entry(self, test_event, inner_content=None):
636 """Registers a testcase result, and returns the text created.
637
638 The caller is expected to manage failure/skip/success counts
639 in some kind of appropriate way. This call simply constructs
640 the XML and appends the returned result to the self.all_results
641 list.
642
643 @param test_event the test event dictionary.
644
645 @param inner_content if specified, gets included in the <testcase>
646 inner section, at the point before stdout and stderr would be
647 included. This is where a <failure/>, <skipped/>, <error/>, etc.
648 could go.
649
650 @return the text of the xml testcase element.
651 """
652
653 # Get elapsed time.
654 test_class = test_event["test_class"]
655 test_name = test_event["test_name"]
656 event_time = test_event["event_time"]
657 time_taken = self.elapsed_time_for_test(
658 test_class, test_name, event_time)
659
660 # Plumb in stdout/stderr once we shift over to only test results.
661 test_stdout = ''
662 test_stderr = ''
663
664 # Formulate the output xml.
665 if not inner_content:
666 inner_content = ""
667 result = (
668 '<testcase classname="{}" name="{}" time="{:.3f}">'
669 '{}{}{}</testcase>'.format(
670 test_class,
671 test_name,
672 time_taken,
673 inner_content,
674 test_stdout,
675 test_stderr))
676
677 # Save the result, update total test count.
678 with self.lock:
679 self.total_test_count += 1
680 self.elements["all"].append(result)
681
682 return result
683
684 def _end_session_internal(self):
685 """Flushes out the report of test executions to form valid xml output.
686
687 xUnit output is in XML. The reporting system cannot complete the
688 formatting of the output without knowing when there is no more input.
689 This call addresses notifcation of the completed test run and thus is
690 when we can finish off the report output.
691 """
692
693 # Figure out the counts line for the testsuite. If we have
694 # been counting either unexpected successes or expected
695 # failures, we'll output those in the counts, at the risk of
696 # being invalidated by a validating test results viewer.
697 # These aren't counted by default so they won't show up unless
698 # the user specified a formatter option to include them.
699 xfail_count = len(self.elements["expected_failures"])
700 xpass_count = len(self.elements["unexpected_successes"])
701 if xfail_count > 0 or xpass_count > 0:
702 extra_testsuite_attributes = (
703 ' expected-failures="{}"'
704 ' unexpected-successes="{}"'.format(xfail_count, xpass_count))
705 else:
706 extra_testsuite_attributes = ""
707
708 # Output the header.
709 self.out_file.write(
710 '<?xml version="1.0" encoding="{}"?>\n'
711 '<testsuite name="{}" tests="{}" errors="{}" failures="{}" '
712 'skip="{}"{}>\n'.format(
713 self.text_encoding,
714 "LLDB test suite",
715 self.total_test_count,
716 len(self.elements["errors"]),
717 len(self.elements["failures"]),
718 len(self.elements["skips"]),
719 extra_testsuite_attributes))
720
721 # Output each of the test result entries.
722 for result in self.elements["all"]:
723 self.out_file.write(result + '\n')
724
725 # Close off the test suite.
726 self.out_file.write('</testsuite>\n')
727
728 super(XunitFormatter, self).end_session()
729
730 def end_session(self):
731 with self.lock:
732 self._end_session_internal()
733
734
735class RawPickledFormatter(ResultsFormatter):
736 """Formats events as a pickled stream.
737
738 The parallel test runner has inferiors pickle their results and send them
739 over a socket back to the parallel test. The parallel test runner then
740 aggregates them into the final results formatter (e.g. xUnit).
741 """
742
743 @classmethod
744 def arg_parser(cls):
745 """@return arg parser used to parse formatter-specific options."""
746 parser = super(RawPickledFormatter, cls).arg_parser()
747 return parser
748
749 def __init__(self, out_file, options):
750 super(RawPickledFormatter, self).__init__(out_file, options)
751 self.pid = os.getpid()
752
753 def begin_session(self):
754 super(RawPickledFormatter, self).begin_session()
755 self.process_event({
756 "event": "session_begin",
757 "event_time": time.time(),
758 "pid": self.pid
759 })
760
761 def process_event(self, test_event):
762 super(RawPickledFormatter, self).process_event(test_event)
763
764 # Add pid to the event for tracking.
765 # test_event["pid"] = self.pid
766
767 # Send it as {serialized_length_of_serialized_bytes}#{serialized_bytes}
768 pickled_message = cPickle.dumps(test_event)
769 self.out_file.send(
770 "{}#{}".format(len(pickled_message), pickled_message))
771
772 def end_session(self):
773 self.process_event({
774 "event": "session_end",
775 "event_time": time.time(),
776 "pid": self.pid
777 })
778 super(RawPickledFormatter, self).end_session()