991 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			991 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
|                      The LLVM Compiler Infrastructure
 | |
| 
 | |
| This file is distributed under the University of Illinois Open Source
 | |
| License. See LICENSE.TXT for details.
 | |
| 
 | |
| Provides classes used by the test results reporting infrastructure
 | |
| within the LLDB test suite.
 | |
| """
 | |
| 
 | |
| from __future__ import print_function
 | |
| from __future__ import absolute_import
 | |
| 
 | |
| # System modules
 | |
| import argparse
 | |
| import inspect
 | |
| import os
 | |
| import pprint
 | |
| import re
 | |
| import sys
 | |
| import threading
 | |
| import time
 | |
| import traceback
 | |
| import xml.sax.saxutils
 | |
| 
 | |
| # Third-party modules
 | |
| import six
 | |
| from six.moves import cPickle
 | |
| 
 | |
| # LLDB modules
 | |
| 
 | |
| class EventBuilder(object):
 | |
|     """Helper class to build test result event dictionaries."""
 | |
| 
 | |
|     BASE_DICTIONARY = None
 | |
| 
 | |
|     @staticmethod
 | |
|     def _get_test_name_info(test):
 | |
|         """Returns (test-class-name, test-method-name) from a test case instance.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @return tuple containing (test class name, test method name)
 | |
|         """
 | |
|         test_class_components = test.id().split(".")
 | |
|         test_class_name = ".".join(test_class_components[:-1])
 | |
|         test_name = test_class_components[-1]
 | |
|         return (test_class_name, test_name)
 | |
| 
 | |
|     @staticmethod
 | |
|     def bare_event(event_type):
 | |
|         """Creates an event with default additions, event type and timestamp.
 | |
| 
 | |
|         @param event_type the value set for the "event" key, used
 | |
|         to distinguish events.
 | |
| 
 | |
|         @returns an event dictionary with all default additions, the "event"
 | |
|         key set to the passed in event_type, and the event_time value set to
 | |
|         time.time().
 | |
|         """
 | |
|         if EventBuilder.BASE_DICTIONARY is not None:
 | |
|             # Start with a copy of the "always include" entries.
 | |
|             event = dict(EventBuilder.BASE_DICTIONARY)
 | |
|         else:
 | |
|             event = {}
 | |
| 
 | |
|         event.update({
 | |
|             "event": event_type,
 | |
|             "event_time": time.time()
 | |
|             })
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def _event_dictionary_common(test, event_type):
 | |
|         """Returns an event dictionary setup with values for the given event type.
 | |
| 
 | |
|         @param test the unittest.TestCase instance
 | |
| 
 | |
|         @param event_type the name of the event type (string).
 | |
| 
 | |
|         @return event dictionary with common event fields set.
 | |
|         """
 | |
|         test_class_name, test_name = EventBuilder._get_test_name_info(test)
 | |
| 
 | |
|         event = EventBuilder.bare_event(event_type)
 | |
|         event.update({
 | |
|             "test_class": test_class_name,
 | |
|             "test_name": test_name,
 | |
|             "test_filename": inspect.getfile(test.__class__)
 | |
|         })
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def _error_tuple_class(error_tuple):
 | |
|         """Returns the unittest error tuple's error class as a string.
 | |
| 
 | |
|         @param error_tuple the error tuple provided by the test framework.
 | |
| 
 | |
|         @return the error type (typically an exception) raised by the
 | |
|         test framework.
 | |
|         """
 | |
|         type_var = error_tuple[0]
 | |
|         module = inspect.getmodule(type_var)
 | |
|         if module:
 | |
|             return "{}.{}".format(module.__name__, type_var.__name__)
 | |
|         else:
 | |
|             return type_var.__name__
 | |
| 
 | |
|     @staticmethod
 | |
|     def _error_tuple_message(error_tuple):
 | |
|         """Returns the unittest error tuple's error message.
 | |
| 
 | |
|         @param error_tuple the error tuple provided by the test framework.
 | |
| 
 | |
|         @return the error message provided by the test framework.
 | |
|         """
 | |
|         return str(error_tuple[1])
 | |
| 
 | |
|     @staticmethod
 | |
|     def _error_tuple_traceback(error_tuple):
 | |
|         """Returns the unittest error tuple's error message.
 | |
| 
 | |
|         @param error_tuple the error tuple provided by the test framework.
 | |
| 
 | |
|         @return the error message provided by the test framework.
 | |
|         """
 | |
|         return error_tuple[2]
 | |
| 
 | |
|     @staticmethod
 | |
|     def _event_dictionary_test_result(test, status):
 | |
|         """Returns an event dictionary with common test result fields set.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param status the status/result of the test
 | |
|         (e.g. "success", "failure", etc.)
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_common(test, "test_result")
 | |
|         event["status"] = status
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def _event_dictionary_issue(test, status, error_tuple):
 | |
|         """Returns an event dictionary with common issue-containing test result
 | |
|         fields set.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param status the status/result of the test
 | |
|         (e.g. "success", "failure", etc.)
 | |
| 
 | |
|         @param error_tuple the error tuple as reported by the test runner.
 | |
|         This is of the form (type<error>, error).
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_test_result(test, status)
 | |
|         event["issue_class"] = EventBuilder._error_tuple_class(error_tuple)
 | |
|         event["issue_message"] = EventBuilder._error_tuple_message(error_tuple)
 | |
|         backtrace = EventBuilder._error_tuple_traceback(error_tuple)
 | |
|         if backtrace is not None:
 | |
|             event["issue_backtrace"] = traceback.format_tb(backtrace)
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_start(test):
 | |
|         """Returns an event dictionary for the test start event.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         return EventBuilder._event_dictionary_common(test, "test_start")
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_success(test):
 | |
|         """Returns an event dictionary for a successful test.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         return EventBuilder._event_dictionary_test_result(test, "success")
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_unexpected_success(test, bugnumber):
 | |
|         """Returns an event dictionary for a test that succeeded but was
 | |
|         expected to fail.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param bugnumber the issue identifier for the bug tracking the
 | |
|         fix request for the test expected to fail (but is in fact
 | |
|         passing here).
 | |
| 
 | |
|         @return the event dictionary
 | |
| 
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_test_result(
 | |
|             test, "unexpected_success")
 | |
|         if bugnumber:
 | |
|             event["bugnumber"] = str(bugnumber)
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_failure(test, error_tuple):
 | |
|         """Returns an event dictionary for a test that failed.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param error_tuple the error tuple as reported by the test runner.
 | |
|         This is of the form (type<error>, error).
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         return EventBuilder._event_dictionary_issue(
 | |
|             test, "failure", error_tuple)
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_expected_failure(test, error_tuple, bugnumber):
 | |
|         """Returns an event dictionary for a test that failed as expected.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param error_tuple the error tuple as reported by the test runner.
 | |
|         This is of the form (type<error>, error).
 | |
| 
 | |
|         @param bugnumber the issue identifier for the bug tracking the
 | |
|         fix request for the test expected to fail.
 | |
| 
 | |
|         @return the event dictionary
 | |
| 
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_issue(
 | |
|             test, "expected_failure", error_tuple)
 | |
|         if bugnumber:
 | |
|             event["bugnumber"] = str(bugnumber)
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_skip(test, reason):
 | |
|         """Returns an event dictionary for a test that was skipped.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param reason the reason why the test is being skipped.
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_test_result(test, "skip")
 | |
|         event["skip_reason"] = reason
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_error(test, error_tuple):
 | |
|         """Returns an event dictionary for a test that hit a test execution error.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param error_tuple the error tuple as reported by the test runner.
 | |
|         This is of the form (type<error>, error).
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         return EventBuilder._event_dictionary_issue(test, "error", error_tuple)
 | |
| 
 | |
|     @staticmethod
 | |
|     def event_for_cleanup_error(test, error_tuple):
 | |
|         """Returns an event dictionary for a test that hit a test execution error
 | |
|         during the test cleanup phase.
 | |
| 
 | |
|         @param test a unittest.TestCase instance.
 | |
| 
 | |
|         @param error_tuple the error tuple as reported by the test runner.
 | |
|         This is of the form (type<error>, error).
 | |
| 
 | |
|         @return the event dictionary
 | |
|         """
 | |
|         event = EventBuilder._event_dictionary_issue(
 | |
|             test, "error", error_tuple)
 | |
|         event["issue_phase"] = "cleanup"
 | |
|         return event
 | |
| 
 | |
|     @staticmethod
 | |
|     def add_entries_to_all_events(entries_dict):
 | |
|         """Specifies a dictionary of entries to add to all test events.
 | |
| 
 | |
|         This provides a mechanism for, say, a parallel test runner to
 | |
|         indicate to each inferior dotest.py that it should add a
 | |
|         worker index to each.
 | |
| 
 | |
|         Calling this method replaces all previous entries added
 | |
|         by a prior call to this.
 | |
| 
 | |
|         Event build methods will overwrite any entries that collide.
 | |
|         Thus, the passed in dictionary is the base, which gets merged
 | |
|         over by event building when keys collide.
 | |
| 
 | |
|         @param entries_dict a dictionary containing key and value
 | |
|         pairs that should be merged into all events created by the
 | |
|         event generator.  May be None to clear out any extra entries.
 | |
|         """
 | |
|         EventBuilder.BASE_DICTIONARY = dict(entries_dict)
 | |
| 
 | |
| 
 | |
| class ResultsFormatter(object):
 | |
| 
 | |
|     """Provides interface to formatting test results out to a file-like object.
 | |
| 
 | |
|     This class allows the LLDB test framework's raw test-realted
 | |
|     events to be processed and formatted in any manner desired.
 | |
|     Test events are represented by python dictionaries, formatted
 | |
|     as in the EventBuilder class above.
 | |
| 
 | |
|     ResultFormatter instances are given a file-like object in which
 | |
|     to write their results.
 | |
| 
 | |
|     ResultFormatter lifetime looks like the following:
 | |
| 
 | |
|     # The result formatter is created.
 | |
|     # The argparse options dictionary is generated from calling
 | |
|     # the SomeResultFormatter.arg_parser() with the options data
 | |
|     # passed to dotest.py via the "--results-formatter-options"
 | |
|     # argument.  See the help on that for syntactic requirements
 | |
|     # on getting that parsed correctly.
 | |
|     formatter = SomeResultFormatter(file_like_object, argpared_options_dict)
 | |
| 
 | |
|     # Single call to session start, before parsing any events.
 | |
|     formatter.begin_session()
 | |
| 
 | |
|     formatter.handle_event({"event":"initialize",...})
 | |
| 
 | |
|     # Zero or more calls specified for events recorded during the test session.
 | |
|     # The parallel test runner manages getting results from all the inferior
 | |
|     # dotest processes, so from a new format perspective, don't worry about
 | |
|     # that.  The formatter will be presented with a single stream of events
 | |
|     # sandwiched between a single begin_session()/end_session() pair in the
 | |
|     # parallel test runner process/thread.
 | |
|     for event in zero_or_more_test_events():
 | |
|         formatter.handle_event(event)
 | |
| 
 | |
|     # Single call to terminate/wrap-up. Formatters that need all the
 | |
|     # data before they can print a correct result (e.g. xUnit/JUnit),
 | |
|     # this is where the final report can be generated.
 | |
|     formatter.handle_event({"event":"terminate",...})
 | |
| 
 | |
|     It is not the formatter's responsibility to close the file_like_object.
 | |
|     (i.e. do not close it).
 | |
| 
 | |
|     The lldb test framework passes these test events in real time, so they
 | |
|     arrive as they come in.
 | |
| 
 | |
|     In the case of the parallel test runner, the dotest inferiors
 | |
|     add a 'pid' field to the dictionary that indicates which inferior
 | |
|     pid generated the event.
 | |
| 
 | |
|     Note more events may be added in the future to support richer test
 | |
|     reporting functionality. One example: creating a true flaky test
 | |
|     result category so that unexpected successes really mean the test
 | |
|     is marked incorrectly (either should be marked flaky, or is indeed
 | |
|     passing consistently now and should have the xfail marker
 | |
|     removed). In this case, a flaky_success and flaky_fail event
 | |
|     likely will be added to capture these and support reporting things
 | |
|     like percentages of flaky test passing so we can see if we're
 | |
|     making some things worse/better with regards to failure rates.
 | |
| 
 | |
|     Another example: announcing all the test methods that are planned
 | |
|     to be run, so we can better support redo operations of various kinds
 | |
|     (redo all non-run tests, redo non-run tests except the one that
 | |
|     was running [perhaps crashed], etc.)
 | |
| 
 | |
|     Implementers are expected to override all the public methods
 | |
|     provided in this class. See each method's docstring to see
 | |
|     expectations about when the call should be chained.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     @classmethod
 | |
|     def arg_parser(cls):
 | |
|         """@return arg parser used to parse formatter-specific options."""
 | |
|         parser = argparse.ArgumentParser(
 | |
|             description='{} options'.format(cls.__name__),
 | |
|             usage=('dotest.py --results-formatter-options='
 | |
|                    '"--option1 value1 [--option2 value2 [...]]"'))
 | |
|         return parser
 | |
| 
 | |
|     def __init__(self, out_file, options):
 | |
|         super(ResultsFormatter, self).__init__()
 | |
|         self.out_file = out_file
 | |
|         self.options = options
 | |
|         self.using_terminal = False
 | |
|         if not self.out_file:
 | |
|             raise Exception("ResultsFormatter created with no file object")
 | |
|         self.start_time_by_test = {}
 | |
|         self.terminate_called = False
 | |
| 
 | |
|         # Lock that we use while mutating inner state, like the
 | |
|         # total test count and the elements.  We minimize how
 | |
|         # long we hold the lock just to keep inner state safe, not
 | |
|         # entirely consistent from the outside.
 | |
|         self.lock = threading.Lock()
 | |
| 
 | |
|     def handle_event(self, test_event):
 | |
|         """Handles the test event for collection into the formatter output.
 | |
| 
 | |
|         Derived classes may override this but should call down to this
 | |
|         implementation first.
 | |
| 
 | |
|         @param test_event the test event as formatted by one of the
 | |
|         event_for_* calls.
 | |
|         """
 | |
|         # Keep track of whether terminate was received.  We do this so
 | |
|         # that a process can call the 'terminate' event on its own, to
 | |
|         # close down a formatter at the appropriate time.  Then the
 | |
|         # atexit() cleanup can call the "terminate if it hasn't been
 | |
|         # called yet".
 | |
|         if test_event is not None:
 | |
|             if test_event.get("event", "") == "terminate":
 | |
|                 self.terminate_called = True
 | |
| 
 | |
|     def track_start_time(self, test_class, test_name, start_time):
 | |
|         """Tracks the start time of a test so elapsed time can be computed.
 | |
| 
 | |
|         This alleviates the need for test results to be processed serially
 | |
|         by test.  It will save the start time for the test so that
 | |
|         elapsed_time_for_test() can compute the elapsed time properly.
 | |
|         """
 | |
|         if test_class is None or test_name is None:
 | |
|             return
 | |
| 
 | |
|         test_key = "{}.{}".format(test_class, test_name)
 | |
|         with self.lock:
 | |
|             self.start_time_by_test[test_key] = start_time
 | |
| 
 | |
|     def elapsed_time_for_test(self, test_class, test_name, end_time):
 | |
|         """Returns the elapsed time for a test.
 | |
| 
 | |
|         This function can only be called once per test and requires that
 | |
|         the track_start_time() method be called sometime prior to calling
 | |
|         this method.
 | |
|         """
 | |
|         if test_class is None or test_name is None:
 | |
|             return -2.0
 | |
| 
 | |
|         test_key = "{}.{}".format(test_class, test_name)
 | |
|         with self.lock:
 | |
|             if test_key not in self.start_time_by_test:
 | |
|                 return -1.0
 | |
|             else:
 | |
|                 start_time = self.start_time_by_test[test_key]
 | |
|             del self.start_time_by_test[test_key]
 | |
|         return end_time - start_time
 | |
| 
 | |
|     def is_using_terminal(self):
 | |
|         """Returns True if this results formatter is using the terminal and
 | |
|         output should be avoided."""
 | |
|         return self.using_terminal
 | |
| 
 | |
|     def send_terminate_as_needed(self):
 | |
|         """Sends the terminate event if it hasn't been received yet."""
 | |
|         if not self.terminate_called:
 | |
|             terminate_event = EventBuilder.bare_event("terminate")
 | |
|             self.handle_event(terminate_event)
 | |
| 
 | |
| 
 | |
| class XunitFormatter(ResultsFormatter):
 | |
|     """Provides xUnit-style formatted output.
 | |
|     """
 | |
| 
 | |
|     # Result mapping arguments
 | |
|     RM_IGNORE = 'ignore'
 | |
|     RM_SUCCESS = 'success'
 | |
|     RM_FAILURE = 'failure'
 | |
|     RM_PASSTHRU = 'passthru'
 | |
| 
 | |
|     @staticmethod
 | |
|     def _build_illegal_xml_regex():
 | |
|         """Contructs a regex to match all illegal xml characters.
 | |
| 
 | |
|         Expects to be used against a unicode string."""
 | |
|         # Construct the range pairs of invalid unicode chareacters.
 | |
|         illegal_chars_u = [
 | |
|             (0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84),
 | |
|             (0x86, 0x9F), (0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)]
 | |
| 
 | |
|         # For wide builds, we have more.
 | |
|         if sys.maxunicode >= 0x10000:
 | |
|             illegal_chars_u.extend(
 | |
|                 [(0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF),
 | |
|                  (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF),
 | |
|                  (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF),
 | |
|                  (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF),
 | |
|                  (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF),
 | |
|                  (0x10FFFE, 0x10FFFF)])
 | |
| 
 | |
|         # Build up an array of range expressions.
 | |
|         illegal_ranges = [
 | |
|             "%s-%s" % (six.unichr(low), six.unichr(high))
 | |
|             for (low, high) in illegal_chars_u]
 | |
| 
 | |
|         # Compile the regex
 | |
|         return re.compile(six.u('[%s]') % six.u('').join(illegal_ranges))
 | |
| 
 | |
|     @staticmethod
 | |
|     def _quote_attribute(text):
 | |
|         """Returns the given text in a manner safe for usage in an XML attribute.
 | |
| 
 | |
|         @param text the text that should appear within an XML attribute.
 | |
|         @return the attribute-escaped version of the input text.
 | |
|         """
 | |
|         return xml.sax.saxutils.quoteattr(text)
 | |
| 
 | |
|     def _replace_invalid_xml(self, str_or_unicode):
 | |
|         """Replaces invalid XML characters with a '?'.
 | |
| 
 | |
|         @param str_or_unicode a string to replace invalid XML
 | |
|         characters within.  Can be unicode or not.  If not unicode,
 | |
|         assumes it is a byte string in utf-8 encoding.
 | |
| 
 | |
|         @returns a utf-8-encoded byte string with invalid
 | |
|         XML replaced with '?'.
 | |
|         """
 | |
|         # Get the content into unicode
 | |
|         if isinstance(str_or_unicode, str):
 | |
|             unicode_content = str_or_unicode.decode('utf-8')
 | |
|         else:
 | |
|             unicode_content = str_or_unicode
 | |
|         return self.invalid_xml_re.sub(six.u('?'), unicode_content).encode('utf-8')
 | |
| 
 | |
|     @classmethod
 | |
|     def arg_parser(cls):
 | |
|         """@return arg parser used to parse formatter-specific options."""
 | |
|         parser = super(XunitFormatter, cls).arg_parser()
 | |
| 
 | |
|         # These are valid choices for results mapping.
 | |
|         results_mapping_choices = [
 | |
|             XunitFormatter.RM_IGNORE,
 | |
|             XunitFormatter.RM_SUCCESS,
 | |
|             XunitFormatter.RM_FAILURE,
 | |
|             XunitFormatter.RM_PASSTHRU]
 | |
|         parser.add_argument(
 | |
|             "--assert-on-unknown-events",
 | |
|             action="store_true",
 | |
|             help=('cause unknown test events to generate '
 | |
|                   'a python assert.  Default is to ignore.'))
 | |
|         parser.add_argument(
 | |
|             "--ignore-skip-name",
 | |
|             "-n",
 | |
|             metavar='PATTERN',
 | |
|             action="append",
 | |
|             dest='ignore_skip_name_patterns',
 | |
|             help=('a python regex pattern, where '
 | |
|                   'any skipped test with a test method name where regex '
 | |
|                   'matches (via search) will be ignored for xUnit test '
 | |
|                   'result purposes.  Can be specified multiple times.'))
 | |
|         parser.add_argument(
 | |
|             "--ignore-skip-reason",
 | |
|             "-r",
 | |
|             metavar='PATTERN',
 | |
|             action="append",
 | |
|             dest='ignore_skip_reason_patterns',
 | |
|             help=('a python regex pattern, where '
 | |
|                   'any skipped test with a skip reason where the regex '
 | |
|                   'matches (via search) will be ignored for xUnit test '
 | |
|                   'result purposes.  Can be specified multiple times.'))
 | |
|         parser.add_argument(
 | |
|             "--xpass", action="store", choices=results_mapping_choices,
 | |
|             default=XunitFormatter.RM_FAILURE,
 | |
|             help=('specify mapping from unexpected success to jUnit/xUnit '
 | |
|                   'result type'))
 | |
|         parser.add_argument(
 | |
|             "--xfail", action="store", choices=results_mapping_choices,
 | |
|             default=XunitFormatter.RM_IGNORE,
 | |
|             help=('specify mapping from expected failure to jUnit/xUnit '
 | |
|                   'result type'))
 | |
|         return parser
 | |
| 
 | |
|     @staticmethod
 | |
|     def _build_regex_list_from_patterns(patterns):
 | |
|         """Builds a list of compiled regexes from option value.
 | |
| 
 | |
|         @param option string containing a comma-separated list of regex
 | |
|         patterns. Zero-length or None will produce an empty regex list.
 | |
| 
 | |
|         @return list of compiled regular expressions, empty if no
 | |
|         patterns provided.
 | |
|         """
 | |
|         regex_list = []
 | |
|         if patterns is not None:
 | |
|             for pattern in patterns:
 | |
|                 regex_list.append(re.compile(pattern))
 | |
|         return regex_list
 | |
| 
 | |
|     def __init__(self, out_file, options):
 | |
|         """Initializes the XunitFormatter instance.
 | |
|         @param out_file file-like object where formatted output is written.
 | |
|         @param options_dict specifies a dictionary of options for the
 | |
|         formatter.
 | |
|         """
 | |
|         # Initialize the parent
 | |
|         super(XunitFormatter, self).__init__(out_file, options)
 | |
|         self.text_encoding = "UTF-8"
 | |
|         self.invalid_xml_re = XunitFormatter._build_illegal_xml_regex()
 | |
|         self.total_test_count = 0
 | |
|         self.ignore_skip_name_regexes = (
 | |
|             XunitFormatter._build_regex_list_from_patterns(
 | |
|                 options.ignore_skip_name_patterns))
 | |
|         self.ignore_skip_reason_regexes = (
 | |
|             XunitFormatter._build_regex_list_from_patterns(
 | |
|                 options.ignore_skip_reason_patterns))
 | |
| 
 | |
|         self.elements = {
 | |
|             "successes": [],
 | |
|             "errors": [],
 | |
|             "failures": [],
 | |
|             "skips": [],
 | |
|             "unexpected_successes": [],
 | |
|             "expected_failures": [],
 | |
|             "all": []
 | |
|             }
 | |
| 
 | |
|         self.status_handlers = {
 | |
|             "success": self._handle_success,
 | |
|             "failure": self._handle_failure,
 | |
|             "error": self._handle_error,
 | |
|             "skip": self._handle_skip,
 | |
|             "expected_failure": self._handle_expected_failure,
 | |
|             "unexpected_success": self._handle_unexpected_success
 | |
|             }
 | |
| 
 | |
|     def handle_event(self, test_event):
 | |
|         super(XunitFormatter, self).handle_event(test_event)
 | |
| 
 | |
|         event_type = test_event["event"]
 | |
|         if event_type is None:
 | |
|             return
 | |
| 
 | |
|         if event_type == "terminate":
 | |
|             self._finish_output()
 | |
|         elif event_type == "test_start":
 | |
|             self.track_start_time(
 | |
|                 test_event["test_class"],
 | |
|                 test_event["test_name"],
 | |
|                 test_event["event_time"])
 | |
|         elif event_type == "test_result":
 | |
|             self._process_test_result(test_event)
 | |
|         else:
 | |
|             # This is an unknown event.
 | |
|             if self.options.assert_on_unknown_events:
 | |
|                 raise Exception("unknown event type {} from {}\n".format(
 | |
|                     event_type, test_event))
 | |
| 
 | |
|     def _handle_success(self, test_event):
 | |
|         """Handles a test success.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
|         result = self._common_add_testcase_entry(test_event)
 | |
|         with self.lock:
 | |
|             self.elements["successes"].append(result)
 | |
| 
 | |
|     def _handle_failure(self, test_event):
 | |
|         """Handles a test failure.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
|         message = self._replace_invalid_xml(test_event["issue_message"])
 | |
|         backtrace = self._replace_invalid_xml(
 | |
|             "".join(test_event.get("issue_backtrace", [])))
 | |
| 
 | |
|         result = self._common_add_testcase_entry(
 | |
|             test_event,
 | |
|             inner_content=(
 | |
|                 '<failure type={} message={}><![CDATA[{}]]></failure>'.format(
 | |
|                     XunitFormatter._quote_attribute(test_event["issue_class"]),
 | |
|                     XunitFormatter._quote_attribute(message),
 | |
|                     backtrace)
 | |
|             ))
 | |
|         with self.lock:
 | |
|             self.elements["failures"].append(result)
 | |
| 
 | |
|     def _handle_error(self, test_event):
 | |
|         """Handles a test error.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
|         message = self._replace_invalid_xml(test_event["issue_message"])
 | |
|         backtrace = self._replace_invalid_xml(
 | |
|             "".join(test_event.get("issue_backtrace", [])))
 | |
| 
 | |
|         result = self._common_add_testcase_entry(
 | |
|             test_event,
 | |
|             inner_content=(
 | |
|                 '<error type={} message={}><![CDATA[{}]]></error>'.format(
 | |
|                     XunitFormatter._quote_attribute(test_event["issue_class"]),
 | |
|                     XunitFormatter._quote_attribute(message),
 | |
|                     backtrace)
 | |
|             ))
 | |
|         with self.lock:
 | |
|             self.elements["errors"].append(result)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _ignore_based_on_regex_list(test_event, test_key, regex_list):
 | |
|         """Returns whether to ignore a test event based on patterns.
 | |
| 
 | |
|         @param test_event the test event dictionary to check.
 | |
|         @param test_key the key within the dictionary to check.
 | |
|         @param regex_list a list of zero or more regexes.  May contain
 | |
|         zero or more compiled regexes.
 | |
| 
 | |
|         @return True if any o the regex list match based on the
 | |
|         re.search() method; false otherwise.
 | |
|         """
 | |
|         for regex in regex_list:
 | |
|             match = regex.search(test_event.get(test_key, ''))
 | |
|             if match:
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
|     def _handle_skip(self, test_event):
 | |
|         """Handles a skipped test.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
| 
 | |
|         # Are we ignoring this test based on test name?
 | |
|         if XunitFormatter._ignore_based_on_regex_list(
 | |
|                 test_event, 'test_name', self.ignore_skip_name_regexes):
 | |
|             return
 | |
| 
 | |
|         # Are we ignoring this test based on skip reason?
 | |
|         if XunitFormatter._ignore_based_on_regex_list(
 | |
|                 test_event, 'skip_reason', self.ignore_skip_reason_regexes):
 | |
|             return
 | |
| 
 | |
|         # We're not ignoring this test.  Process the skip.
 | |
|         reason = self._replace_invalid_xml(test_event.get("skip_reason", ""))
 | |
|         result = self._common_add_testcase_entry(
 | |
|             test_event,
 | |
|             inner_content='<skipped message={} />'.format(
 | |
|                 XunitFormatter._quote_attribute(reason)))
 | |
|         with self.lock:
 | |
|             self.elements["skips"].append(result)
 | |
| 
 | |
|     def _handle_expected_failure(self, test_event):
 | |
|         """Handles a test that failed as expected.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
|         if self.options.xfail == XunitFormatter.RM_PASSTHRU:
 | |
|             # This is not a natively-supported junit/xunit
 | |
|             # testcase mode, so it might fail a validating
 | |
|             # test results viewer.
 | |
|             if "bugnumber" in test_event:
 | |
|                 bug_id_attribute = 'bug-id={} '.format(
 | |
|                     XunitFormatter._quote_attribute(test_event["bugnumber"]))
 | |
|             else:
 | |
|                 bug_id_attribute = ''
 | |
| 
 | |
|             result = self._common_add_testcase_entry(
 | |
|                 test_event,
 | |
|                 inner_content=(
 | |
|                     '<expected-failure {}type={} message={} />'.format(
 | |
|                         bug_id_attribute,
 | |
|                         XunitFormatter._quote_attribute(
 | |
|                             test_event["issue_class"]),
 | |
|                         XunitFormatter._quote_attribute(
 | |
|                             test_event["issue_message"]))
 | |
|                 ))
 | |
|             with self.lock:
 | |
|                 self.elements["expected_failures"].append(result)
 | |
|         elif self.options.xfail == XunitFormatter.RM_SUCCESS:
 | |
|             result = self._common_add_testcase_entry(test_event)
 | |
|             with self.lock:
 | |
|                 self.elements["successes"].append(result)
 | |
|         elif self.options.xfail == XunitFormatter.RM_FAILURE:
 | |
|             result = self._common_add_testcase_entry(
 | |
|                 test_event,
 | |
|                 inner_content='<failure type={} message={} />'.format(
 | |
|                     XunitFormatter._quote_attribute(test_event["issue_class"]),
 | |
|                     XunitFormatter._quote_attribute(
 | |
|                         test_event["issue_message"])))
 | |
|             with self.lock:
 | |
|                 self.elements["failures"].append(result)
 | |
|         elif self.options.xfail == XunitFormatter.RM_IGNORE:
 | |
|             pass
 | |
|         else:
 | |
|             raise Exception(
 | |
|                 "unknown xfail option: {}".format(self.options.xfail))
 | |
| 
 | |
|     def _handle_unexpected_success(self, test_event):
 | |
|         """Handles a test that passed but was expected to fail.
 | |
|         @param test_event the test event to handle.
 | |
|         """
 | |
|         if self.options.xpass == XunitFormatter.RM_PASSTHRU:
 | |
|             # This is not a natively-supported junit/xunit
 | |
|             # testcase mode, so it might fail a validating
 | |
|             # test results viewer.
 | |
|             result = self._common_add_testcase_entry(
 | |
|                 test_event,
 | |
|                 inner_content=("<unexpected-success />"))
 | |
|             with self.lock:
 | |
|                 self.elements["unexpected_successes"].append(result)
 | |
|         elif self.options.xpass == XunitFormatter.RM_SUCCESS:
 | |
|             # Treat the xpass as a success.
 | |
|             result = self._common_add_testcase_entry(test_event)
 | |
|             with self.lock:
 | |
|                 self.elements["successes"].append(result)
 | |
|         elif self.options.xpass == XunitFormatter.RM_FAILURE:
 | |
|             # Treat the xpass as a failure.
 | |
|             if "bugnumber" in test_event:
 | |
|                 message = "unexpected success (bug_id:{})".format(
 | |
|                     test_event["bugnumber"])
 | |
|             else:
 | |
|                 message = "unexpected success (bug_id:none)"
 | |
|             result = self._common_add_testcase_entry(
 | |
|                 test_event,
 | |
|                 inner_content='<failure type={} message={} />'.format(
 | |
|                     XunitFormatter._quote_attribute("unexpected_success"),
 | |
|                     XunitFormatter._quote_attribute(message)))
 | |
|             with self.lock:
 | |
|                 self.elements["failures"].append(result)
 | |
|         elif self.options.xpass == XunitFormatter.RM_IGNORE:
 | |
|             # Ignore the xpass result as far as xUnit reporting goes.
 | |
|             pass
 | |
|         else:
 | |
|             raise Exception("unknown xpass option: {}".format(
 | |
|                 self.options.xpass))
 | |
| 
 | |
|     def _process_test_result(self, test_event):
 | |
|         """Processes the test_event known to be a test result.
 | |
| 
 | |
|         This categorizes the event appropriately and stores the data needed
 | |
|         to generate the final xUnit report.  This method skips events that
 | |
|         cannot be represented in xUnit output.
 | |
|         """
 | |
|         if "status" not in test_event:
 | |
|             raise Exception("test event dictionary missing 'status' key")
 | |
| 
 | |
|         status = test_event["status"]
 | |
|         if status not in self.status_handlers:
 | |
|             raise Exception("test event status '{}' unsupported".format(
 | |
|                 status))
 | |
| 
 | |
|         # Call the status handler for the test result.
 | |
|         self.status_handlers[status](test_event)
 | |
| 
 | |
|     def _common_add_testcase_entry(self, test_event, inner_content=None):
 | |
|         """Registers a testcase result, and returns the text created.
 | |
| 
 | |
|         The caller is expected to manage failure/skip/success counts
 | |
|         in some kind of appropriate way.  This call simply constructs
 | |
|         the XML and appends the returned result to the self.all_results
 | |
|         list.
 | |
| 
 | |
|         @param test_event the test event dictionary.
 | |
| 
 | |
|         @param inner_content if specified, gets included in the <testcase>
 | |
|         inner section, at the point before stdout and stderr would be
 | |
|         included.  This is where a <failure/>, <skipped/>, <error/>, etc.
 | |
|         could go.
 | |
| 
 | |
|         @return the text of the xml testcase element.
 | |
|         """
 | |
| 
 | |
|         # Get elapsed time.
 | |
|         test_class = test_event["test_class"]
 | |
|         test_name = test_event["test_name"]
 | |
|         event_time = test_event["event_time"]
 | |
|         time_taken = self.elapsed_time_for_test(
 | |
|             test_class, test_name, event_time)
 | |
| 
 | |
|         # Plumb in stdout/stderr once we shift over to only test results.
 | |
|         test_stdout = ''
 | |
|         test_stderr = ''
 | |
| 
 | |
|         # Formulate the output xml.
 | |
|         if not inner_content:
 | |
|             inner_content = ""
 | |
|         result = (
 | |
|             '<testcase classname="{}" name="{}" time="{:.3f}">'
 | |
|             '{}{}{}</testcase>'.format(
 | |
|                 test_class,
 | |
|                 test_name,
 | |
|                 time_taken,
 | |
|                 inner_content,
 | |
|                 test_stdout,
 | |
|                 test_stderr))
 | |
| 
 | |
|         # Save the result, update total test count.
 | |
|         with self.lock:
 | |
|             self.total_test_count += 1
 | |
|             self.elements["all"].append(result)
 | |
| 
 | |
|         return result
 | |
| 
 | |
|     def _finish_output_no_lock(self):
 | |
|         """Flushes out the report of test executions to form valid xml output.
 | |
| 
 | |
|         xUnit output is in XML.  The reporting system cannot complete the
 | |
|         formatting of the output without knowing when there is no more input.
 | |
|         This call addresses notifcation of the completed test run and thus is
 | |
|         when we can finish off the report output.
 | |
|         """
 | |
| 
 | |
|         # Figure out the counts line for the testsuite.  If we have
 | |
|         # been counting either unexpected successes or expected
 | |
|         # failures, we'll output those in the counts, at the risk of
 | |
|         # being invalidated by a validating test results viewer.
 | |
|         # These aren't counted by default so they won't show up unless
 | |
|         # the user specified a formatter option to include them.
 | |
|         xfail_count = len(self.elements["expected_failures"])
 | |
|         xpass_count = len(self.elements["unexpected_successes"])
 | |
|         if xfail_count > 0 or xpass_count > 0:
 | |
|             extra_testsuite_attributes = (
 | |
|                 ' expected-failures="{}"'
 | |
|                 ' unexpected-successes="{}"'.format(xfail_count, xpass_count))
 | |
|         else:
 | |
|             extra_testsuite_attributes = ""
 | |
| 
 | |
|         # Output the header.
 | |
|         self.out_file.write(
 | |
|             '<?xml version="1.0" encoding="{}"?>\n'
 | |
|             '<testsuites>'
 | |
|             '<testsuite name="{}" tests="{}" errors="{}" failures="{}" '
 | |
|             'skip="{}"{}>\n'.format(
 | |
|                 self.text_encoding,
 | |
|                 "LLDB test suite",
 | |
|                 self.total_test_count,
 | |
|                 len(self.elements["errors"]),
 | |
|                 len(self.elements["failures"]),
 | |
|                 len(self.elements["skips"]),
 | |
|                 extra_testsuite_attributes))
 | |
| 
 | |
|         # Output each of the test result entries.
 | |
|         for result in self.elements["all"]:
 | |
|             self.out_file.write(result + '\n')
 | |
| 
 | |
|         # Close off the test suite.
 | |
|         self.out_file.write('</testsuite></testsuites>\n')
 | |
| 
 | |
|     def _finish_output(self):
 | |
|         """Finish writing output as all incoming events have arrived."""
 | |
|         with self.lock:
 | |
|             self._finish_output_no_lock()
 | |
| 
 | |
| 
 | |
| class RawPickledFormatter(ResultsFormatter):
 | |
|     """Formats events as a pickled stream.
 | |
| 
 | |
|     The parallel test runner has inferiors pickle their results and send them
 | |
|     over a socket back to the parallel test.  The parallel test runner then
 | |
|     aggregates them into the final results formatter (e.g. xUnit).
 | |
|     """
 | |
| 
 | |
|     @classmethod
 | |
|     def arg_parser(cls):
 | |
|         """@return arg parser used to parse formatter-specific options."""
 | |
|         parser = super(RawPickledFormatter, cls).arg_parser()
 | |
|         return parser
 | |
| 
 | |
|     def __init__(self, out_file, options):
 | |
|         super(RawPickledFormatter, self).__init__(out_file, options)
 | |
|         self.pid = os.getpid()
 | |
| 
 | |
|     def handle_event(self, test_event):
 | |
|         super(RawPickledFormatter, self).handle_event(test_event)
 | |
| 
 | |
|         # Convert initialize/terminate events into job_begin/job_end events.
 | |
|         event_type = test_event["event"]
 | |
|         if event_type is None:
 | |
|             return
 | |
| 
 | |
|         if event_type == "initialize":
 | |
|             test_event["event"] = "job_begin"
 | |
|         elif event_type == "terminate":
 | |
|             test_event["event"] = "job_end"
 | |
| 
 | |
|         # Tack on the pid.
 | |
|         test_event["pid"] = self.pid
 | |
| 
 | |
|         # Send it as {serialized_length_of_serialized_bytes}#{serialized_bytes}
 | |
|         pickled_message = cPickle.dumps(test_event)
 | |
|         self.out_file.send(
 | |
|             "{}#{}".format(len(pickled_message), pickled_message))
 | |
| 
 | |
| 
 | |
| class DumpFormatter(ResultsFormatter):
 | |
|     """Formats events to the file as their raw python dictionary format."""
 | |
| 
 | |
|     def handle_event(self, test_event):
 | |
|         super(DumpFormatter, self).handle_event(test_event)
 | |
|         self.out_file.write("\n" + pprint.pformat(test_event) + "\n")
 |