diff options
Diffstat (limited to 'packages/Python/lldbsuite/test_event')
13 files changed, 2659 insertions, 0 deletions
diff --git a/packages/Python/lldbsuite/test_event/__init__.py b/packages/Python/lldbsuite/test_event/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/__init__.py diff --git a/packages/Python/lldbsuite/test_event/build_exception.py b/packages/Python/lldbsuite/test_event/build_exception.py new file mode 100644 index 000000000000..4a7c5f4a9d38 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/build_exception.py @@ -0,0 +1,14 @@ +class BuildError(Exception): +    def __init__(self, called_process_error): +        super(BuildError, self).__init__("Error when building test subject") +        self.command = called_process_error.lldb_extensions.get("command", "<command unavailable>") +        self.build_error = called_process_error.lldb_extensions.get("stderr_content", "<error output unavailable>") + +    def __str__(self): +        return self.format_build_error(self.command, self.build_error) + +    @staticmethod +    def format_build_error(command, command_output): +        return "Error when building test subject.\n\nBuild Command:\n{}\n\nBuild Command Output:\n{}".format( +            command, +            command_output) diff --git a/packages/Python/lldbsuite/test_event/dotest_channels.py b/packages/Python/lldbsuite/test_event/dotest_channels.py new file mode 100644 index 000000000000..d69720e4f66a --- /dev/null +++ b/packages/Python/lldbsuite/test_event/dotest_channels.py @@ -0,0 +1,207 @@ +""" +                     The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. + +Sync lldb and related source from a local machine to a remote machine. + +This facilitates working on the lldb sourcecode on multiple machines +and multiple OS types, verifying changes across all. + + +This module provides asyncore channels used within the LLDB test +framework. +""" + +from __future__ import print_function +from __future__ import absolute_import + + +# System modules +import asyncore +import socket + +# Third-party modules +from six.moves import cPickle + +# LLDB modules + + +class UnpicklingForwardingReaderChannel(asyncore.dispatcher): +    """Provides an unpickling, forwarding asyncore dispatch channel reader. + +    Inferior dotest.py processes with side-channel-based test results will +    send test result event data in a pickled format, one event at a time. +    This class supports reconstructing the pickled data and forwarding it +    on to its final destination. + +    The channel data is written in the form: +    {num_payload_bytes}#{payload_bytes} + +    The bulk of this class is devoted to reading and parsing out +    the payload bytes. +    """ +    def __init__(self, file_object, async_map, forwarding_func): +        asyncore.dispatcher.__init__(self, sock=file_object, map=async_map) + +        self.header_contents = b"" +        self.packet_bytes_remaining = 0 +        self.reading_header = True +        self.ibuffer = b'' +        self.forwarding_func = forwarding_func +        if forwarding_func is None: +            # This whole class is useless if we do nothing with the +            # unpickled results. +            raise Exception("forwarding function must be set") + +        # Initiate all connections by sending an ack.  This allows +        # the initiators of the socket to await this to ensure +        # that this end is up and running (and therefore already +        # into the async map). +        ack_bytes = b'*' +        file_object.send(ack_bytes) + +    def deserialize_payload(self): +        """Unpickles the collected input buffer bytes and forwards.""" +        if len(self.ibuffer) > 0: +            self.forwarding_func(cPickle.loads(self.ibuffer)) +            self.ibuffer = b'' + +    def consume_header_bytes(self, data): +        """Consumes header bytes from the front of data. +        @param data the incoming data stream bytes +        @return any data leftover after consuming header bytes. +        """ +        # We're done if there is no content. +        if not data or (len(data) == 0): +            return None + +        full_header_len = 4 + +        assert len(self.header_contents) < full_header_len + +        bytes_avail = len(data) +        bytes_needed = full_header_len - len(self.header_contents) +        header_bytes_avail = min(bytes_needed, bytes_avail) +        self.header_contents += data[:header_bytes_avail] +        if len(self.header_contents) == full_header_len: +            import struct +            # End of header. +            self.packet_bytes_remaining = struct.unpack( +                "!I", self.header_contents)[0] +            self.header_contents = b"" +            self.reading_header = False +            return data[header_bytes_avail:] + +        # If we made it here, we've exhausted the data and +        # we're still parsing header content. +        return None + +    def consume_payload_bytes(self, data): +        """Consumes payload bytes from the front of data. +        @param data the incoming data stream bytes +        @return any data leftover after consuming remaining payload bytes. +        """ +        if not data or (len(data) == 0): +            # We're done and there's nothing to do. +            return None + +        data_len = len(data) +        if data_len <= self.packet_bytes_remaining: +            # We're consuming all the data provided. +            self.ibuffer += data +            self.packet_bytes_remaining -= data_len + +            # If we're no longer waiting for payload bytes, +            # we flip back to parsing header bytes and we +            # unpickle the payload contents. +            if self.packet_bytes_remaining < 1: +                self.reading_header = True +                self.deserialize_payload() + +            # We're done, no more data left. +            return None +        else: +            # We're only consuming a portion of the data since +            # the data contains more than the payload amount. +            self.ibuffer += data[:self.packet_bytes_remaining] +            data = data[self.packet_bytes_remaining:] + +            # We now move on to reading the header. +            self.reading_header = True +            self.packet_bytes_remaining = 0 + +            # And we can deserialize the payload. +            self.deserialize_payload() + +            # Return the remaining data. +            return data + +    def handle_read(self): +        # Read some data from the socket. +        try: +            data = self.recv(8192) +            # print('driver socket READ: %d bytes' % len(data)) +        except socket.error as socket_error: +            print( +                "\nINFO: received socket error when reading data " +                "from test inferior:\n{}".format(socket_error)) +            raise +        except Exception as general_exception: +            print( +                "\nERROR: received non-socket error when reading data " +                "from the test inferior:\n{}".format(general_exception)) +            raise + +        # Consume the message content. +        while data and (len(data) > 0): +            # If we're reading the header, gather header bytes. +            if self.reading_header: +                data = self.consume_header_bytes(data) +            else: +                data = self.consume_payload_bytes(data) + +    def handle_close(self): +        # print("socket reader: closing port") +        self.close() + + +class UnpicklingForwardingListenerChannel(asyncore.dispatcher): +    """Provides a socket listener asyncore channel for unpickling/forwarding. + +    This channel will listen on a socket port (use 0 for host-selected).  Any +    client that connects will have an UnpicklingForwardingReaderChannel handle +    communication over the connection. + +    The dotest parallel test runners, when collecting test results, open the +    test results side channel over a socket.  This channel handles connections +    from inferiors back to the test runner.  Each worker fires up a listener +    for each inferior invocation.  This simplifies the asyncore.loop() usage, +    one of the reasons for implementing with asyncore.  This listener shuts +    down once a single connection is made to it. +    """ +    def __init__(self, async_map, host, port, backlog_count, forwarding_func): +        asyncore.dispatcher.__init__(self, map=async_map) +        self.create_socket(socket.AF_INET, socket.SOCK_STREAM) +        self.set_reuse_addr() +        self.bind((host, port)) +        self.address = self.socket.getsockname() +        self.listen(backlog_count) +        self.handler = None +        self.async_map = async_map +        self.forwarding_func = forwarding_func +        if forwarding_func is None: +            # This whole class is useless if we do nothing with the +            # unpickled results. +            raise Exception("forwarding function must be set") + +    def handle_accept(self): +        (sock, addr) = self.socket.accept() +        if sock and addr: +            # print('Incoming connection from %s' % repr(addr)) +            self.handler = UnpicklingForwardingReaderChannel( +                sock, self.async_map, self.forwarding_func) + +    def handle_close(self): +        self.close() diff --git a/packages/Python/lldbsuite/test_event/event_builder.py b/packages/Python/lldbsuite/test_event/event_builder.py new file mode 100644 index 000000000000..aabbd986bd70 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/event_builder.py @@ -0,0 +1,475 @@ +""" +    The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. + +Provides a class to build Python test event data structures. +""" + +from __future__ import print_function +from __future__ import absolute_import + +# System modules +import inspect +import time +import traceback + +# Third-party modules + +# LLDB modules +from . import build_exception + +class EventBuilder(object): +    """Helper class to build test result event dictionaries.""" + +    BASE_DICTIONARY = None + +    # Test Event Types +    TYPE_JOB_RESULT = "job_result" +    TYPE_TEST_RESULT = "test_result" +    TYPE_TEST_START = "test_start" +    TYPE_MARK_TEST_RERUN_ELIGIBLE = "test_eligible_for_rerun" +    TYPE_MARK_TEST_EXPECTED_FAILURE = "test_expected_failure" +    TYPE_SESSION_TERMINATE = "terminate" + +    RESULT_TYPES = {TYPE_JOB_RESULT, TYPE_TEST_RESULT} + +    # Test/Job Status Tags +    STATUS_EXCEPTIONAL_EXIT = "exceptional_exit" +    STATUS_SUCCESS = "success" +    STATUS_FAILURE = "failure" +    STATUS_EXPECTED_FAILURE = "expected_failure" +    STATUS_EXPECTED_TIMEOUT = "expected_timeout" +    STATUS_UNEXPECTED_SUCCESS = "unexpected_success" +    STATUS_SKIP = "skip" +    STATUS_ERROR = "error" +    STATUS_TIMEOUT = "timeout" + +    """Test methods or jobs with a status matching any of these +    status values will cause a testrun failure, unless +    the test methods rerun and do not trigger an issue when rerun.""" +    TESTRUN_ERROR_STATUS_VALUES = { +        STATUS_ERROR, +        STATUS_EXCEPTIONAL_EXIT, +        STATUS_FAILURE, +        STATUS_TIMEOUT} + +    @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 _assert_is_python_sourcefile(test_filename): +        if test_filename is not None: +            if not test_filename.endswith(".py"): +                raise Exception("source python filename has unexpected extension: {}".format(test_filename)) +        return test_filename + +    @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) + +        # Determine the filename for the test case.  If there is an attribute +        # for it, use it.  Otherwise, determine from the TestCase class path. +        if hasattr(test, "test_filename"): +            test_filename = EventBuilder._assert_is_python_sourcefile(test.test_filename) +        else: +            test_filename = EventBuilder._assert_is_python_sourcefile(inspect.getsourcefile(test.__class__)) + +        event = EventBuilder.bare_event(event_type) +        event.update({ +            "test_class": test_class_name, +            "test_name": test_name, +            "test_filename": test_filename +        }) + +        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, EventBuilder.TYPE_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, EventBuilder.TYPE_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, EventBuilder.STATUS_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, EventBuilder.STATUS_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, EventBuilder.STATUS_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, EventBuilder.STATUS_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, EventBuilder.STATUS_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 +        """ +        event = EventBuilder._event_dictionary_issue( +            test, EventBuilder.STATUS_ERROR, error_tuple) +        event["issue_phase"] = "test" +        return event + +    @staticmethod +    def event_for_build_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, EventBuilder.STATUS_ERROR, error_tuple) +        event["issue_phase"] = "build" + +        build_error = error_tuple[1] +        event["build_command"] = build_error.command +        event["build_error"] = build_error.build_error +        return event + +    @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, EventBuilder.STATUS_ERROR, error_tuple) +        event["issue_phase"] = "cleanup" +        return event + +    @staticmethod +    def event_for_job_test_add_error(test_filename, exception, backtrace): +        event = EventBuilder.bare_event(EventBuilder.TYPE_JOB_RESULT) +        event["status"] = EventBuilder.STATUS_ERROR +        if test_filename is not None: +            event["test_filename"] = EventBuilder._assert_is_python_sourcefile(test_filename) +        if exception is not None and "__class__" in dir(exception): +            event["issue_class"] = exception.__class__ +        event["issue_message"] = exception +        if backtrace is not None: +            event["issue_backtrace"] = backtrace +        return event + +    @staticmethod +    def event_for_job_exceptional_exit( +            pid, worker_index, exception_code, exception_description, +            test_filename, command_line): +        """Creates an event for a job (i.e. process) exit due to signal. + +        @param pid the process id for the job that failed +        @param worker_index optional id for the job queue running the process +        @param exception_code optional code +        (e.g. SIGTERM integer signal number) +        @param exception_description optional string containing symbolic +        representation of the issue (e.g. "SIGTERM") +        @param test_filename the path to the test filename that exited +        in some exceptional way. +        @param command_line the Popen()-style list provided as the command line +        for the process that timed out. + +        @return an event dictionary coding the job completion description. +        """ +        event = EventBuilder.bare_event(EventBuilder.TYPE_JOB_RESULT) +        event["status"] = EventBuilder.STATUS_EXCEPTIONAL_EXIT +        if pid is not None: +            event["pid"] = pid +        if worker_index is not None: +            event["worker_index"] = int(worker_index) +        if exception_code is not None: +            event["exception_code"] = exception_code +        if exception_description is not None: +            event["exception_description"] = exception_description +        if test_filename is not None: +            event["test_filename"] = EventBuilder._assert_is_python_sourcefile(test_filename) +        if command_line is not None: +            event["command_line"] = command_line +        return event + +    @staticmethod +    def event_for_job_timeout(pid, worker_index, test_filename, command_line): +        """Creates an event for a job (i.e. process) timeout. + +        @param pid the process id for the job that timed out +        @param worker_index optional id for the job queue running the process +        @param test_filename the path to the test filename that timed out. +        @param command_line the Popen-style list provided as the command line +        for the process that timed out. + +        @return an event dictionary coding the job completion description. +        """ +        event = EventBuilder.bare_event(EventBuilder.TYPE_JOB_RESULT) +        event["status"] = "timeout" +        if pid is not None: +            event["pid"] = pid +        if worker_index is not None: +            event["worker_index"] = int(worker_index) +        if test_filename is not None: +            event["test_filename"] = EventBuilder._assert_is_python_sourcefile(test_filename) +        if command_line is not None: +            event["command_line"] = command_line +        return event + +    @staticmethod +    def event_for_mark_test_rerun_eligible(test): +        """Creates an event that indicates the specified test is explicitly +        eligible for rerun. + +        Note there is a mode that will enable test rerun eligibility at the +        global level.  These markings for explicit rerun eligibility are +        intended for the mode of running where only explicitly re-runnable +        tests are rerun upon hitting an issue. + +        @param test the TestCase instance to which this pertains. + +        @return an event that specifies the given test as being eligible to +        be rerun. +        """ +        event = EventBuilder._event_dictionary_common( +            test, +            EventBuilder.TYPE_MARK_TEST_RERUN_ELIGIBLE) +        return event + +    @staticmethod +    def event_for_mark_test_expected_failure(test): +        """Creates an event that indicates the specified test is expected +        to fail. + +        @param test the TestCase instance to which this pertains. + +        @return an event that specifies the given test is expected to fail. +        """ +        event = EventBuilder._event_dictionary_common( +            test, +            EventBuilder.TYPE_MARK_TEST_EXPECTED_FAILURE) +        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) diff --git a/packages/Python/lldbsuite/test_event/formatter/__init__.py b/packages/Python/lldbsuite/test_event/formatter/__init__.py new file mode 100644 index 000000000000..556370ebb9de --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/__init__.py @@ -0,0 +1,162 @@ +""" +    The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. +""" + +from __future__ import print_function +from __future__ import absolute_import + +# System modules +import importlib +import socket +import sys + +# Third-party modules + +# LLDB modules + + +# Ignore method count on DTOs. +# pylint: disable=too-few-public-methods +class FormatterConfig(object): +    """Provides formatter configuration info to create_results_formatter().""" + +    def __init__(self): +        self.filename = None +        self.port = None +        self.formatter_name = None +        self.formatter_options = None + + +# Ignore method count on DTOs. +# pylint: disable=too-few-public-methods +class CreatedFormatter(object): +    """Provides transfer object for returns from create_results_formatter().""" + +    def __init__(self, formatter, cleanup_func): +        self.formatter = formatter +        self.cleanup_func = cleanup_func + + +def create_results_formatter(config): +    """Sets up a test results formatter. + +    @param config an instance of FormatterConfig +    that indicates how to setup the ResultsFormatter. + +    @return an instance of CreatedFormatter. +    """ + +    def create_socket(port): +        """Creates a socket to the localhost on the given port. + +        @param port the port number of the listening port on +        the localhost. + +        @return (socket object, socket closing function) +        """ + +        def socket_closer(open_sock): +            """Close down an opened socket properly.""" +            open_sock.shutdown(socket.SHUT_RDWR) +            open_sock.close() + +        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +        sock.connect(("localhost", port)) + +        # Wait for the ack from the listener side. +        # This is needed to prevent a race condition +        # in the main dosep.py processing loop: we +        # can't allow a worker queue thread to die +        # that has outstanding messages to a listener +        # socket before the listener socket asyncore +        # listener socket gets spun up; otherwise, +        # we lose the test result info. +        read_bytes = sock.recv(1) +        if read_bytes is None or (len(read_bytes) < 1) or (read_bytes != b'*'): +            raise Exception("listening socket did not respond with ack byte: response={}".format(read_bytes)) + +        return sock, lambda: socket_closer(sock) + +    default_formatter_name = None +    results_file_object = None +    cleanup_func = None + +    file_is_stream = False +    if config.filename: +        # Open the results file for writing. +        if config.filename == 'stdout': +            results_file_object = sys.stdout +            cleanup_func = None +        elif config.filename == 'stderr': +            results_file_object = sys.stderr +            cleanup_func = None +        else: +            results_file_object = open(config.filename, "w") +            cleanup_func = results_file_object.close +        default_formatter_name = ( +            "lldbsuite.test_event.formatter.xunit.XunitFormatter") +    elif config.port: +        # Connect to the specified localhost port. +        results_file_object, cleanup_func = create_socket(config.port) +        default_formatter_name = ( +            "lldbsuite.test_event.formatter.pickled.RawPickledFormatter") +        file_is_stream = True + +    # If we have a results formatter name specified and we didn't specify +    # a results file, we should use stdout. +    if config.formatter_name is not None and results_file_object is None: +        # Use stdout. +        results_file_object = sys.stdout +        cleanup_func = None + +    if results_file_object: +        # We care about the formatter.  Choose user-specified or, if +        # none specified, use the default for the output type. +        if config.formatter_name: +            formatter_name = config.formatter_name +        else: +            formatter_name = default_formatter_name + +        # Create an instance of the class. +        # First figure out the package/module. +        components = formatter_name.split(".") +        module = importlib.import_module(".".join(components[:-1])) + +        # Create the class name we need to load. +        cls = getattr(module, components[-1]) + +        # Handle formatter options for the results formatter class. +        formatter_arg_parser = cls.arg_parser() +        if config.formatter_options and len(config.formatter_options) > 0: +            command_line_options = config.formatter_options +        else: +            command_line_options = [] + +        formatter_options = formatter_arg_parser.parse_args( +            command_line_options) + +        # Create the TestResultsFormatter given the processed options. +        results_formatter_object = cls( +            results_file_object, +            formatter_options, +            file_is_stream) + +        def shutdown_formatter(): +            """Shuts down the formatter when it is no longer needed.""" +            # Tell the formatter to write out anything it may have +            # been saving until the very end (e.g. xUnit results +            # can't complete its output until this point). +            results_formatter_object.send_terminate_as_needed() + +            # And now close out the output file-like object. +            if cleanup_func is not None: +                cleanup_func() + +        return CreatedFormatter( +            results_formatter_object, +            shutdown_formatter) +    else: +        return None diff --git a/packages/Python/lldbsuite/test_event/formatter/curses.py b/packages/Python/lldbsuite/test_event/formatter/curses.py new file mode 100644 index 000000000000..4e89fa9bf01e --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/curses.py @@ -0,0 +1,229 @@ +""" +                     The LLVM Compiler Infrastructure + + This file is distributed under the University of Illinois Open Source + License. See LICENSE.TXT for details. +""" + +from __future__ import absolute_import +from __future__ import print_function + +# System modules +import curses +import datetime +import math +import sys +import time + +# Third-party modules + +# LLDB modules +from lldbsuite.test import lldbcurses + +from . import results_formatter +from ..event_builder import EventBuilder + + +class Curses(results_formatter.ResultsFormatter): +    """Receives live results from tests that are running and reports them to the terminal in a curses GUI""" + +    def __init__(self, out_file, options, file_is_stream): +        # Initialize the parent +        super(Curses, self).__init__(out_file, options, file_is_stream) +        self.using_terminal = True +        self.have_curses = True +        self.initialize_event = None +        self.jobs = [None] * 64 +        self.job_tests = [None] * 64 +        self.results = list() +        try: +            self.main_window = lldbcurses.intialize_curses() +            self.main_window.add_key_action('\t', self.main_window.select_next_first_responder, "Switch between views that can respond to keyboard input") +            self.main_window.refresh() +            self.job_panel = None +            self.results_panel = None +            self.status_panel = None +            self.info_panel = None +            self.hide_status_list = list() +            self.start_time = time.time() +        except: +            self.have_curses = False +            lldbcurses.terminate_curses() +            self.using_terminal = False +            print("Unexpected error:", sys.exc_info()[0]) +            raise + +        self.line_dict = dict() +        # self.events_file = open("/tmp/events.txt", "w") +        # self.formatters = list() +        # if tee_results_formatter: +        #     self.formatters.append(tee_results_formatter) + +    def status_to_short_str(self, status, test_event): +        if status == EventBuilder.STATUS_SUCCESS: +            return '.' +        elif status == EventBuilder.STATUS_FAILURE: +            return 'F' +        elif status == EventBuilder.STATUS_UNEXPECTED_SUCCESS: +            return '?' +        elif status == EventBuilder.STATUS_EXPECTED_FAILURE: +            return 'X' +        elif status == EventBuilder.STATUS_SKIP: +            return 'S' +        elif status == EventBuilder.STATUS_ERROR: +            if test_event.get("issue_phase", None) == "build": +                # Build failure +                return 'B' +            else: +                return 'E' +        elif status == EventBuilder.STATUS_TIMEOUT: +            return 'T' +        elif status == EventBuilder.STATUS_EXPECTED_TIMEOUT: +            return 't' +        else: +            return status + +    def show_info_panel(self): +        selected_idx = self.results_panel.get_selected_idx() +        if selected_idx >= 0 and selected_idx < len(self.results): +            if self.info_panel is None: +                info_frame = self.results_panel.get_contained_rect(top_inset=10, left_inset=10, right_inset=10, height=30) +                self.info_panel = lldbcurses.BoxedPanel(info_frame, "Result Details") +                # Add a key action for any key that will hide this panel when any key is pressed +                self.info_panel.add_key_action(-1, self.hide_info_panel, 'Hide the info panel') +                self.info_panel.top() +            else: +                self.info_panel.show() + +            self.main_window.push_first_responder(self.info_panel) +            test_start = self.results[selected_idx][0] +            test_result = self.results[selected_idx][1] +            self.info_panel.set_line(0, "File: %s" % (test_start['test_filename'])) +            self.info_panel.set_line(1, "Test: %s.%s" % (test_start['test_class'], test_start['test_name'])) +            self.info_panel.set_line(2, "Time: %s" % (test_result['elapsed_time'])) +            self.info_panel.set_line(3, "Status: %s" % (test_result['status'])) + +    def hide_info_panel(self): +        self.main_window.pop_first_responder(self.info_panel) +        self.info_panel.hide() +        self.main_window.refresh() + +    def toggle_status(self, status): +        if status: +            # Toggle showing and hiding results whose status matches "status" in "Results" window +            if status in self.hide_status_list: +                self.hide_status_list.remove(status) +            else: +                self.hide_status_list.append(status) +            self.update_results() + +    def update_results(self, update=True): +        '''Called after a category of test have been show/hidden to update the results list with +           what the user desires to see.''' +        self.results_panel.clear(update=False) +        for result in self.results: +            test_result = result[1] +            status = test_result['status'] +            if status in self.hide_status_list: +                continue +            name = test_result['test_class'] + '.' + test_result['test_name'] +            self.results_panel.append_line('%s (%6.2f sec) %s' % (self.status_to_short_str(status, test_result), test_result['elapsed_time'], name)) +        if update: +            self.main_window.refresh() + +    def handle_event(self, test_event): +        with self.lock: +            super(Curses, self).handle_event(test_event) +            # for formatter in self.formatters: +            #     formatter.process_event(test_event) +            if self.have_curses: +                worker_index = -1 +                if 'worker_index' in test_event: +                    worker_index = test_event['worker_index'] +                if 'event' in test_event: +                    check_for_one_key = True +                    #print(str(test_event), file=self.events_file) +                    event = test_event['event'] +                    if self.status_panel: +                        self.status_panel.update_status('time', str(datetime.timedelta(seconds=math.floor(time.time() - self.start_time)))) +                    if event == 'test_start': +                        name = test_event['test_class'] + '.' + test_event['test_name'] +                        self.job_tests[worker_index] = test_event +                        if 'pid' in test_event: +                            line = 'pid: %5d ' % (test_event['pid']) + name +                        else: +                            line = name +                        self.job_panel.set_line(worker_index, line) +                        self.main_window.refresh() +                    elif event == 'test_result': +                        status = test_event['status'] +                        self.status_panel.increment_status(status) +                        if 'pid' in test_event: +                            line = 'pid: %5d ' % (test_event['pid']) +                        else: +                            line = '' +                        self.job_panel.set_line(worker_index, line) +                        name = test_event['test_class'] + '.' + test_event['test_name'] +                        elapsed_time = test_event['event_time'] - self.job_tests[worker_index]['event_time'] +                        if not status in self.hide_status_list: +                            self.results_panel.append_line('%s (%6.2f sec) %s' % (self.status_to_short_str(status, test_event), elapsed_time, name)) +                        self.main_window.refresh() +                        # Append the result pairs +                        test_event['elapsed_time'] = elapsed_time +                        self.results.append([self.job_tests[worker_index], test_event]) +                        self.job_tests[worker_index] = '' +                    elif event == 'job_begin': +                        self.jobs[worker_index] = test_event +                        if 'pid' in test_event: +                            line = 'pid: %5d ' % (test_event['pid']) +                        else: +                            line = '' +                        self.job_panel.set_line(worker_index, line) +                    elif event == 'job_end': +                        self.jobs[worker_index] = '' +                        self.job_panel.set_line(worker_index, '') +                    elif event == 'initialize': +                        self.initialize_event = test_event +                        num_jobs = test_event['worker_count'] +                        job_frame = self.main_window.get_contained_rect(height=num_jobs+2) +                        results_frame = self.main_window.get_contained_rect(top_inset=num_jobs+2, bottom_inset=1) +                        status_frame = self.main_window.get_contained_rect(height=1, top_inset=self.main_window.get_size().h-1) +                        self.job_panel = lldbcurses.BoxedPanel(frame=job_frame, title="Jobs") +                        self.results_panel = lldbcurses.BoxedPanel(frame=results_frame, title="Results") + +                        self.results_panel.add_key_action(curses.KEY_UP,    self.results_panel.select_prev      , "Select the previous list entry") +                        self.results_panel.add_key_action(curses.KEY_DOWN,  self.results_panel.select_next      , "Select the next list entry") +                        self.results_panel.add_key_action(curses.KEY_HOME,  self.results_panel.scroll_begin     , "Scroll to the start of the list") +                        self.results_panel.add_key_action(curses.KEY_END,   self.results_panel.scroll_end       , "Scroll to the end of the list") +                        self.results_panel.add_key_action(curses.KEY_ENTER, self.show_info_panel                , "Display info for the selected result item") +                        self.results_panel.add_key_action('.', lambda : self.toggle_status(EventBuilder.STATUS_SUCCESS)           , "Toggle showing/hiding tests whose status is 'success'") +                        self.results_panel.add_key_action('e', lambda : self.toggle_status(EventBuilder.STATUS_ERROR)             , "Toggle showing/hiding tests whose status is 'error'") +                        self.results_panel.add_key_action('f', lambda : self.toggle_status(EventBuilder.STATUS_FAILURE)           , "Toggle showing/hiding tests whose status is 'failure'") +                        self.results_panel.add_key_action('s', lambda : self.toggle_status(EventBuilder.STATUS_SKIP)              , "Toggle showing/hiding tests whose status is 'skip'") +                        self.results_panel.add_key_action('x', lambda : self.toggle_status(EventBuilder.STATUS_EXPECTED_FAILURE)  , "Toggle showing/hiding tests whose status is 'expected_failure'") +                        self.results_panel.add_key_action('?', lambda : self.toggle_status(EventBuilder.STATUS_UNEXPECTED_SUCCESS), "Toggle showing/hiding tests whose status is 'unexpected_success'") +                        self.status_panel = lldbcurses.StatusPanel(frame=status_frame) + +                        self.main_window.add_child(self.job_panel) +                        self.main_window.add_child(self.results_panel) +                        self.main_window.add_child(self.status_panel) +                        self.main_window.set_first_responder(self.results_panel) + +                        self.status_panel.add_status_item(name="time", title="Elapsed", format="%s", width=20, value="0:00:00", update=False) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_SUCCESS, title="Success", format="%u", width=20, value=0, update=False) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_FAILURE, title="Failure", format="%u", width=20, value=0, update=False) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_ERROR, title="Error", format="%u", width=20, value=0, update=False) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_SKIP, title="Skipped", format="%u", width=20, value=0, update=True) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_EXPECTED_FAILURE, title="Expected Failure", format="%u", width=30, value=0, update=False) +                        self.status_panel.add_status_item(name=EventBuilder.STATUS_UNEXPECTED_SUCCESS, title="Unexpected Success", format="%u", width=30, value=0, update=False) +                        self.main_window.refresh() +                    elif event == 'terminate': +                        #self.main_window.key_event_loop() +                        lldbcurses.terminate_curses() +                        check_for_one_key = False +                        self.using_terminal = False +                        # Check for 1 keypress with no delay + +                    # Check for 1 keypress with no delay +                    if check_for_one_key: +                        self.main_window.key_event_loop(0, 1) diff --git a/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py b/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py new file mode 100644 index 000000000000..d42dcb18bb4f --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py @@ -0,0 +1,23 @@ +""" +    The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. +""" + +from __future__ import print_function +from __future__ import absolute_import + +# System modules +import pprint + +# Our modules +from .results_formatter import ResultsFormatter + + +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") diff --git a/packages/Python/lldbsuite/test_event/formatter/pickled.py b/packages/Python/lldbsuite/test_event/formatter/pickled.py new file mode 100644 index 000000000000..6d800f6c8baa --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/pickled.py @@ -0,0 +1,72 @@ +""" +    The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. +""" + +from __future__ import print_function +from __future__ import absolute_import + +# System modules +import os + +# Our modules +from .results_formatter import ResultsFormatter +from six.moves import cPickle + + +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 + +    class StreamSerializer(object): +        @staticmethod +        def serialize(test_event, out_file): +            # Send it as {serialized_length_of_serialized_bytes}{serialized_bytes} +            import struct +            msg = cPickle.dumps(test_event) +            packet = struct.pack("!I%ds" % len(msg), len(msg), msg) +            out_file.send(packet) + +    class BlockSerializer(object): +        @staticmethod +        def serialize(test_event, out_file): +            cPickle.dump(test_event, out_file) + +    def __init__(self, out_file, options, file_is_stream): +        super(RawPickledFormatter, self).__init__(out_file, options, file_is_stream) +        self.pid = os.getpid() +        if file_is_stream: +            self.serializer = self.StreamSerializer() +        else: +            self.serializer = self.BlockSerializer() + +    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 + +        # Serialize the test event. +        self.serializer.serialize(test_event, self.out_file) diff --git a/packages/Python/lldbsuite/test_event/formatter/results_formatter.py b/packages/Python/lldbsuite/test_event/formatter/results_formatter.py new file mode 100644 index 000000000000..3bf389b97266 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/results_formatter.py @@ -0,0 +1,717 @@ +""" +                     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 os +import sys +import threading + +# Third-party modules + + +# LLDB modules +from lldbsuite.test import configuration +from ..event_builder import EventBuilder + +import lldbsuite + + +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-related +    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, argparse_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. For 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 [...]]"')) +        parser.add_argument( +            "--dump-results", +            action="store_true", +            help=('dump the raw results data after printing ' +                  'the summary output.')) +        return parser + +    def __init__(self, out_file, options, file_is_stream): +        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 +        self.file_is_stream = file_is_stream + +        # Store counts of test_result events by status. +        self.result_status_counts = { +            EventBuilder.STATUS_SUCCESS: 0, +            EventBuilder.STATUS_EXPECTED_FAILURE: 0, +            EventBuilder.STATUS_EXPECTED_TIMEOUT: 0, +            EventBuilder.STATUS_SKIP: 0, +            EventBuilder.STATUS_UNEXPECTED_SUCCESS: 0, +            EventBuilder.STATUS_FAILURE: 0, +            EventBuilder.STATUS_ERROR: 0, +            EventBuilder.STATUS_TIMEOUT: 0, +            EventBuilder.STATUS_EXCEPTIONAL_EXIT: 0 +        } + +        # Track the most recent test start event by worker index. +        # We'll use this to assign TIMEOUT and exceptional +        # exits to the most recent test started on a given +        # worker index. +        self.started_tests_by_worker = {} + +        # Store the most recent test_method/job status. +        self.result_events = {} + +        # Track the number of test method reruns. +        self.test_method_rerun_count = 0 + +        # 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.RLock() + +        # Keeps track of the test base filenames for tests that +        # are expected to timeout.  If a timeout occurs in any test +        # basename that matches this list, that result should be +        # converted into a non-issue.  We'll create an expected +        # timeout test status for this. +        self.expected_timeouts_by_basename = set() + +        # Tests which have reported that they are expecting to fail. These will +        # be marked as expected failures even if they return a failing status, +        # probably because they crashed or deadlocked. +        self.expected_failures = set() + +        # Keep track of rerun-eligible tests. +        # This is a set that contains tests saved as: +        # {test_filename}:{test_class}:{test_name} +        self.rerun_eligible_tests = set() + +        # A dictionary of test files that had a failing +        # test, in the format of: +        # key = test path, value = array of test methods that need rerun +        self.tests_for_rerun = {} + +    @classmethod +    def _make_key(cls, result_event): +        """Creates a key from a test or job result event. + +        This key attempts to be as unique as possible.  For +        test result events, it will be unique per test method. +        For job events (ones not promoted to a test result event), +        it will be unique per test case file. + +        @return a string-based key of the form +        {test_filename}:{test_class}.{test_name} +        """ +        if result_event is None: +            return None +        component_count = 0 +        if "test_filename" in result_event: +            key = result_event["test_filename"] +            component_count += 1 +        else: +            key = "<no_filename>" +        if "test_class" in result_event: +            if component_count > 0: +                key += ":" +            key += result_event["test_class"] +            component_count += 1 +        if "test_name" in result_event: +            if component_count > 0: +                key += "." +            key += result_event["test_name"] +            component_count += 1 +        return key + +    def _mark_test_as_expected_failure(self, test_result_event): +        key = self._make_key(test_result_event) +        if key is not None: +            self.expected_failures.add(key) +        else: +            sys.stderr.write( +                "\nerror: test marked as expected failure but " +                "failed to create key.\n") + +    def _mark_test_for_rerun_eligibility(self, test_result_event): +        key = self._make_key(test_result_event) +        if key is not None: +            self.rerun_eligible_tests.add(key) +        else: +            sys.stderr.write( +                "\nerror: test marked for re-run eligibility but " +                "failed to create key.\n") + +    def _maybe_add_test_to_rerun_list(self, result_event): +        key = self._make_key(result_event) +        if key is not None: +            if (key in self.rerun_eligible_tests or +                    configuration.rerun_all_issues): +                test_filename = result_event.get("test_filename", None) +                if test_filename is not None: +                    test_name = result_event.get("test_name", None) +                    if test_filename not in self.tests_for_rerun: +                        self.tests_for_rerun[test_filename] = [] +                    if test_name is not None: +                        self.tests_for_rerun[test_filename].append(test_name) +        else: +            sys.stderr.write( +                "\nerror: couldn't add testrun-failing test to rerun " +                "list because no eligibility key could be created.\n") + +    def _maybe_remap_job_result_event(self, test_event): +        """Remaps timeout/exceptional exit job results to last test method running. + +        @param test_event the job_result test event.  This is an in/out +        parameter.  It will be modified if it can be mapped to a test_result +        of the same status, using details from the last-running test method +        known to be most recently started on the same worker index. +        """ +        test_start = None + +        job_status = test_event["status"] +        if job_status in [ +                EventBuilder.STATUS_TIMEOUT, +                EventBuilder.STATUS_EXCEPTIONAL_EXIT]: +            worker_index = test_event.get("worker_index", None) +            if worker_index is not None: +                test_start = self.started_tests_by_worker.get( +                    worker_index, None) + +        # If we have a test start to remap, do it here. +        if test_start is not None: +            test_event["event"] = EventBuilder.TYPE_TEST_RESULT + +            # Fill in all fields from test start not present in +            # job status message. +            for (start_key, start_value) in test_start.items(): +                if start_key not in test_event: +                    test_event[start_key] = start_value + +    def _maybe_remap_expected_timeout(self, event): +        if event is None: +            return + +        status = event.get("status", None) +        if status is None or status != EventBuilder.STATUS_TIMEOUT: +            return + +        # Check if the timeout test's basename is in the expected timeout +        # list.  If so, convert to an expected timeout. +        basename = os.path.basename(event.get("test_filename", "")) +        if basename in self.expected_timeouts_by_basename: +            # Convert to an expected timeout. +            event["status"] = EventBuilder.STATUS_EXPECTED_TIMEOUT + +    def _maybe_remap_expected_failure(self, event): +        if event is None: +            return + +        key = self._make_key(event) +        if key not in self.expected_failures: +            return + +        status = event.get("status", None) +        if status in EventBuilder.TESTRUN_ERROR_STATUS_VALUES: +            event["status"] = EventBuilder.STATUS_EXPECTED_FAILURE +        elif status == EventBuilder.STATUS_SUCCESS: +            event["status"] = EventBuilder.STATUS_UNEXPECTED_SUCCESS + +    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. +        """ +        with self.lock: +            # 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: +                event_type = test_event.get("event", "") +                # We intentionally allow event_type to be checked anew +                # after this check below since this check may rewrite +                # the event type +                if event_type == EventBuilder.TYPE_JOB_RESULT: +                    # Possibly convert the job status (timeout, exceptional exit) +                    # to an appropriate test_result event. +                    self._maybe_remap_job_result_event(test_event) +                    event_type = test_event.get("event", "") + +                # Remap timeouts to expected timeouts. +                if event_type in EventBuilder.RESULT_TYPES: +                    self._maybe_remap_expected_timeout(test_event) +                    self._maybe_remap_expected_failure(test_event) +                    event_type = test_event.get("event", "") + +                if event_type == "terminate": +                    self.terminate_called = True +                elif event_type in EventBuilder.RESULT_TYPES: +                    # Keep track of event counts per test/job result status type. +                    # The only job (i.e. inferior process) results that make it +                    # here are ones that cannot be remapped to the most recently +                    # started test for the given worker index. +                    status = test_event["status"] +                    self.result_status_counts[status] += 1 +                    # Clear the most recently started test for the related worker. +                    worker_index = test_event.get("worker_index", None) +                    if worker_index is not None: +                        self.started_tests_by_worker.pop(worker_index, None) + +                    if status in EventBuilder.TESTRUN_ERROR_STATUS_VALUES: +                        # A test/job status value in any of those status values +                        # causes a testrun failure.  If such a test fails, check +                        # whether it can be rerun.  If it can be rerun, add it +                        # to the rerun job. +                        self._maybe_add_test_to_rerun_list(test_event) + +                    # Build the test key. +                    test_key = self._make_key(test_event) +                    if test_key is None: +                        raise Exception( +                            "failed to find test filename for " +                            "test event {}".format(test_event)) + +                    # Save the most recent test event for the test key. +                    # This allows a second test phase to overwrite the most +                    # recent result for the test key (unique per method). +                    # We do final reporting at the end, so we'll report based +                    # on final results. +                    # We do this so that a re-run caused by, perhaps, the need +                    # to run a low-load, single-worker test run can have the final +                    # run's results to always be used. +                    if test_key in self.result_events: +                        # We are replacing the result of something that was +                        # already counted by the base class.  Remove the double +                        # counting by reducing by one the count for the test +                        # result status. +                        old_status = self.result_events[test_key]["status"] +                        self.result_status_counts[old_status] -= 1 +                        self.test_method_rerun_count += 1 +                    self.result_events[test_key] = test_event +                elif event_type == EventBuilder.TYPE_TEST_START: +                    # Track the start time for the test method. +                    self.track_start_time( +                        test_event["test_class"], +                        test_event["test_name"], +                        test_event["event_time"]) +                    # Track of the most recent test method start event +                    # for the related worker.  This allows us to figure +                    # out whether a process timeout or exceptional exit +                    # can be charged (i.e. assigned) to a test method. +                    worker_index = test_event.get("worker_index", None) +                    if worker_index is not None: +                        self.started_tests_by_worker[worker_index] = test_event + +                elif event_type == EventBuilder.TYPE_MARK_TEST_RERUN_ELIGIBLE: +                    self._mark_test_for_rerun_eligibility(test_event) +                elif event_type == EventBuilder.TYPE_MARK_TEST_EXPECTED_FAILURE: +                    self._mark_test_as_expected_failure(test_event) + +    def set_expected_timeouts_by_basename(self, basenames): +        """Specifies a list of test file basenames that are allowed to timeout +        without being called out as a timeout issue. + +        These fall into a new status category called STATUS_EXPECTED_TIMEOUT. +        """ +        if basenames is not None: +            for basename in basenames: +                self.expected_timeouts_by_basename.add(basename) + +    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) +        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) +        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) + +    # Derived classes may require self access +    # pylint: disable=no-self-use +    # noinspection PyMethodMayBeStatic,PyMethodMayBeStatic +    def replaces_summary(self): +        """Returns whether the results formatter includes a summary +        suitable to replace the old lldb test run results. + +        @return True if the lldb test runner can skip its summary +        generation when using this results formatter; False otherwise. +        """ +        return False + +    def counts_by_test_result_status(self, status): +        """Returns number of test method results for the given status. + +        @status_result a test result status (e.g. success, fail, skip) +        as defined by the EventBuilder.STATUS_* class members. + +        @return an integer returning the number of test methods matching +        the given test result status. +        """ +        return self.result_status_counts[status] + +    @classmethod +    def _event_sort_key(cls, event): +        """Returns the sort key to be used for a test event. + +        This method papers over the differences in a test method result vs. a +        job (i.e. inferior process) result. + +        @param event a test result or job result event. +        @return a key useful for sorting events by name (test name preferably, +        then by test filename). +        """ +        if "test_name" in event: +            return event["test_name"] +        else: +            return event.get("test_filename", None) + +    def _partition_results_by_status(self, categories): +        """Partitions the captured test results by event status. + +        This permits processing test results by the category ids. + +        @param categories the list of categories on which to partition. +        Follows the format described in _report_category_details(). + +        @return a dictionary where each key is the test result status, +        and each entry is a list containing all the test result events +        that matched that test result status.  Result status IDs with +        no matching entries will have a zero-length list. +        """ +        partitioned_events = {} +        for category in categories: +            result_status_id = category[0] +            matching_events = [ +                [key, event] for (key, event) in self.result_events.items() +                if event.get("status", "") == result_status_id] +            partitioned_events[result_status_id] = sorted( +                matching_events, +                key=lambda x: self._event_sort_key(x[1])) +        return partitioned_events + +    @staticmethod +    def _print_banner(out_file, banner_text): +        """Prints an ASCII banner around given text. + +        Output goes to the out file for the results formatter. + +        @param out_file a file-like object where output will be written. +        @param banner_text the text to display, with a banner +        of '=' around the line above and line below. +        """ +        banner_separator = "".ljust(len(banner_text), "=") + +        out_file.write("\n{}\n{}\n{}\n".format( +            banner_separator, +            banner_text, +            banner_separator)) + +    def _print_summary_counts( +            self, out_file, categories, result_events_by_status, extra_rows): +        """Prints summary counts for all categories. + +        @param out_file a file-like object used to print output. + +        @param categories the list of categories on which to partition. +        Follows the format described in _report_category_details(). + +        @param result_events_by_status the partitioned list of test +        result events in a dictionary, with the key set to the test +        result status id and the value set to the list of test method +        results that match the status id. +        """ + +        # Get max length for category printed name +        category_with_max_printed_name = max( +            categories, key=lambda x: len(x[1])) +        max_category_name_length = len(category_with_max_printed_name[1]) + +        # If we are provided with extra rows, consider these row name lengths. +        if extra_rows is not None: +            for row in extra_rows: +                name_length = len(row[0]) +                if name_length > max_category_name_length: +                    max_category_name_length = name_length + +        self._print_banner(out_file, "Test Result Summary") + +        # Prepend extra rows +        if extra_rows is not None: +            for row in extra_rows: +                extra_label = "{}:".format(row[0]).ljust( +                    max_category_name_length + 1) +                out_file.write("{} {:4}\n".format(extra_label, row[1])) + +        for category in categories: +            result_status_id = category[0] +            result_label = "{}:".format(category[1]).ljust( +                max_category_name_length + 1) +            count = len(result_events_by_status[result_status_id]) +            out_file.write("{} {:4}\n".format( +                result_label, +                count)) + +    @classmethod +    def _has_printable_details(cls, categories, result_events_by_status): +        """Returns whether there are any test result details that need to be printed. + +        This will spin through the results and see if any result in a category +        that is printable has any results to print. + +        @param categories the list of categories on which to partition. +        Follows the format described in _report_category_details(). + +        @param result_events_by_status the partitioned list of test +        result events in a dictionary, with the key set to the test +        result status id and the value set to the list of test method +        results that match the status id. + +        @return True if there are any details (i.e. test results +        for failures, errors, unexpected successes); False otherwise. +        """ +        for category in categories: +            result_status_id = category[0] +            print_matching_tests = category[2] +            if print_matching_tests: +                if len(result_events_by_status[result_status_id]) > 0: +                    # We found a printable details test result status +                    # that has details to print. +                    return True +        # We didn't find any test result category with printable +        # details. +        return False + +    @staticmethod +    def _report_category_details(out_file, category, result_events_by_status): +        """Reports all test results matching the given category spec. + +        @param out_file a file-like object used to print output. + +        @param category a category spec of the format [test_event_name, +        printed_category_name, print_matching_entries?] + +        @param result_events_by_status the partitioned list of test +        result events in a dictionary, with the key set to the test +        result status id and the value set to the list of test method +        results that match the status id. +        """ +        result_status_id = category[0] +        print_matching_tests = category[2] +        detail_label = category[3] + +        if print_matching_tests: +            # Sort by test name +            for (_, event) in result_events_by_status[result_status_id]: +                # Convert full test path into test-root-relative. +                test_relative_path = os.path.relpath( +                    os.path.realpath(event["test_filename"]), +                    lldbsuite.lldb_test_root) + +                # Create extra info component (used for exceptional exit info) +                if result_status_id == EventBuilder.STATUS_EXCEPTIONAL_EXIT: +                    extra_info = "[EXCEPTIONAL EXIT {} ({})] ".format( +                        event["exception_code"], +                        event["exception_description"]) +                else: +                    extra_info = "" + +                # Figure out the identity we will use for this test. +                if configuration.verbose and ("test_class" in event): +                    test_id = "{}.{}".format( +                        event["test_class"], event["test_name"]) +                elif "test_name" in event: +                    test_id = event["test_name"] +                else: +                    test_id = "<no_running_test_method>" + +                # Display the info. +                out_file.write("{}: {}{} ({})\n".format( +                    detail_label, +                    extra_info, +                    test_id, +                    test_relative_path)) + +    def print_results(self, out_file): +        """Writes the test result report to the output file. + +        @param out_file a file-like object used for printing summary +        results.  This is different than self.out_file, which might +        be something else for non-summary data. +        """ +        extra_results = [ +            # Total test methods processed, excluding reruns. +            ["Test Methods", len(self.result_events)], +            ["Reruns", self.test_method_rerun_count]] + +        # Output each of the test result entries. +        categories = [ +            # result id, printed name, print matching tests?, detail label +            [EventBuilder.STATUS_SUCCESS, +             "Success", False, None], +            [EventBuilder.STATUS_EXPECTED_FAILURE, +             "Expected Failure", False, None], +            [EventBuilder.STATUS_FAILURE, +             "Failure", True, "FAIL"], +            [EventBuilder.STATUS_ERROR, +             "Error", True, "ERROR"], +            [EventBuilder.STATUS_EXCEPTIONAL_EXIT, +             "Exceptional Exit", True, "ERROR"], +            [EventBuilder.STATUS_UNEXPECTED_SUCCESS, +             "Unexpected Success", True, "UNEXPECTED SUCCESS"], +            [EventBuilder.STATUS_SKIP, "Skip", False, None], +            [EventBuilder.STATUS_TIMEOUT, +             "Timeout", True, "TIMEOUT"], +            [EventBuilder.STATUS_EXPECTED_TIMEOUT, +             # Intentionally using the unusual hyphenation in TIME-OUT to +             # prevent buildbots from thinking it is an issue when scanning +             # for TIMEOUT. +             "Expected Timeout", True, "EXPECTED TIME-OUT"] +            ] + +        # Partition all the events by test result status +        result_events_by_status = self._partition_results_by_status( +            categories) + +        # Print the details +        have_details = self._has_printable_details( +            categories, result_events_by_status) +        if have_details: +            self._print_banner(out_file, "Issue Details") +            for category in categories: +                self._report_category_details( +                    out_file, category, result_events_by_status) + +        # Print the summary +        self._print_summary_counts( +            out_file, categories, result_events_by_status, extra_results) + +        if self.options.dump_results: +            # Debug dump of the key/result info for all categories. +            self._print_banner(out_file, "Results Dump") +            for status, events_by_key in result_events_by_status.items(): +                out_file.write("\nSTATUS: {}\n".format(status)) +                for key, event in events_by_key: +                    out_file.write("key:   {}\n".format(key)) +                    out_file.write("event: {}\n".format(event)) diff --git a/packages/Python/lldbsuite/test_event/formatter/xunit.py b/packages/Python/lldbsuite/test_event/formatter/xunit.py new file mode 100644 index 000000000000..b3682f8fb107 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/formatter/xunit.py @@ -0,0 +1,592 @@ +""" +                     The LLVM Compiler Infrastructure + +This file is distributed under the University of Illinois Open Source +License. See LICENSE.TXT for details. + +Provides an xUnit ResultsFormatter for integrating the LLDB +test suite with the Jenkins xUnit aggregator and other xUnit-compliant +test output processors. +""" +from __future__ import absolute_import +from __future__ import print_function + +# System modules +import re +import sys +import xml.sax.saxutils + +# Third-party modules +import six + +# Local modules +from ..event_builder import EventBuilder +from ..build_exception import BuildError +from .results_formatter import ResultsFormatter + + +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(): +        """Constructs a regex to match all illegal xml characters. + +        Expects to be used against a unicode string.""" +        # Construct the range pairs of invalid unicode characters. +        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 regular expressions from option value. + +        @param patterns contains a list of regular expression +        patterns. + +        @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, file_is_stream): +        """Initializes the XunitFormatter instance. +        @param out_file file-like object where formatted output is written. +        @param options specifies a dictionary of options for the +        formatter. +        """ +        # Initialize the parent +        super(XunitFormatter, self).__init__(out_file, options, file_is_stream) +        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 = { +            EventBuilder.STATUS_SUCCESS: self._handle_success, +            EventBuilder.STATUS_FAILURE: self._handle_failure, +            EventBuilder.STATUS_ERROR: self._handle_error, +            EventBuilder.STATUS_SKIP: self._handle_skip, +            EventBuilder.STATUS_EXPECTED_FAILURE: +                self._handle_expected_failure, +            EventBuilder.STATUS_EXPECTED_TIMEOUT: +                self._handle_expected_timeout, +            EventBuilder.STATUS_UNEXPECTED_SUCCESS: +                self._handle_unexpected_success, +            EventBuilder.STATUS_EXCEPTIONAL_EXIT: +                self._handle_exceptional_exit, +            EventBuilder.STATUS_TIMEOUT: +                self._handle_timeout +            } + +    RESULT_TYPES = {EventBuilder.TYPE_TEST_RESULT, EventBuilder.TYPE_JOB_RESULT} + +    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": +            # Process all the final result events into their +            # XML counterparts. +            for result_event in self.result_events.values(): +                self._process_test_result(result_event) +            self._finish_output() +        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_build(self, test_event): +        """Handles a test error. +        @param test_event the test event to handle. +        """ +        message = self._replace_invalid_xml(test_event["issue_message"]) +        build_issue_description = self._replace_invalid_xml( +            BuildError.format_build_error( +                test_event.get("build_command", "<None>"), +                test_event.get("build_error", "<None>"))) + +        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), +                    build_issue_description) +            )) +        with self.lock: +            self.elements["errors"].append(result) + +    def _handle_error_standard(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) + +    def _handle_error(self, test_event): +        if test_event.get("issue_phase", None) == "build": +            self._handle_error_build(test_event) +        else: +            self._handle_error_standard(test_event) + +    def _handle_exceptional_exit(self, test_event): +        """Handles an exceptional exit. +        @param test_event the test method or job result event to handle. +        """ +        if "test_name" in test_event: +            name = test_event["test_name"] +        else: +            name = test_event.get("test_filename", "<unknown test/filename>") + +        message_text = "ERROR: {} ({}): {}".format( +            test_event.get("exception_code", 0), +            test_event.get("exception_description", ""), +            name) +        message = self._replace_invalid_xml(message_text) + +        result = self._common_add_testcase_entry( +            test_event, +            inner_content=( +                '<error type={} message={}></error>'.format( +                    "exceptional_exit", +                    XunitFormatter._quote_attribute(message)) +            )) +        with self.lock: +            self.elements["errors"].append(result) + +    def _handle_timeout(self, test_event): +        """Handles a test method or job timeout. +        @param test_event the test method or job result event to handle. +        """ +        if "test_name" in test_event: +            name = test_event["test_name"] +        else: +            name = test_event.get("test_filename", "<unknown test/filename>") + +        message_text = "TIMEOUT: {}".format(name) +        message = self._replace_invalid_xml(message_text) + +        result = self._common_add_testcase_entry( +            test_event, +            inner_content=( +                '<error type={} message={}></error>'.format( +                    "timeout", +                    XunitFormatter._quote_attribute(message)) +            )) +        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)) + +    @staticmethod +    def _handle_expected_timeout(test_event): +        """Handles expected_timeout. +        @param test_event the test event to handle. +        """ +        # We don't do anything with expected timeouts, not even report. +        pass + +    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.get("test_class", "<no_class>") +        test_name = test_event.get("test_name", "<no_test_method>") +        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 notification 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() diff --git a/packages/Python/lldbsuite/test_event/test/resources/invalid_decorator/TestInvalidDecorator.py b/packages/Python/lldbsuite/test_event/test/resources/invalid_decorator/TestInvalidDecorator.py new file mode 100644 index 000000000000..7f5c4cb79cf5 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/test/resources/invalid_decorator/TestInvalidDecorator.py @@ -0,0 +1,13 @@ +from __future__ import print_function +from lldbsuite.test import lldbtest +from lldbsuite.test import decorators + + +class NonExistentDecoratorTestCase(lldbtest.TestBase): + +    mydir = lldbtest.TestBase.compute_mydir(__file__) + +    @decorators.nonExistentDecorator(bugnumber="yt/1300") +    def test(self): +        """Verify non-existent decorators are picked up by test runner.""" +        pass diff --git a/packages/Python/lldbsuite/test_event/test/src/TestCatchInvalidDecorator.py b/packages/Python/lldbsuite/test_event/test/src/TestCatchInvalidDecorator.py new file mode 100644 index 000000000000..56814416c333 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/test/src/TestCatchInvalidDecorator.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +""" +Tests that the event system reports issues during decorator +handling as errors. +""" +# System-provided imports +import os +import unittest + +# Local-provided imports +import event_collector + + +class TestCatchInvalidDecorator(unittest.TestCase): + +    TEST_DIR = os.path.join( +        os.path.dirname(__file__), +        os.path.pardir, +        "resources", +        "invalid_decorator") + +    def test_with_whole_file(self): +        """ +        Test that a non-existent decorator generates a test-event error +        when running all tests in the file. +        """ +        # Determine the test case file we're using. +        test_file = os.path.join(self.TEST_DIR, "TestInvalidDecorator.py") + +        # Collect all test events generated for this file. +        error_results = _filter_error_results( +            event_collector.collect_events_whole_file(test_file)) + +        self.assertGreater( +            len(error_results), +            0, +            "At least one job or test error result should have been returned") + +    def test_with_function_filter(self): +        """ +        Test that a non-existent decorator generates a test-event error +        when running a filtered test. +        """ +        # Collect all test events generated during running of tests +        # in a given directory using a test name filter.  Internally, +        # this runs through a different code path that needs to be +        # set up to catch exceptions. +        error_results = _filter_error_results( +            event_collector.collect_events_for_directory_with_filter( +                self.TEST_DIR, +                "NonExistentDecoratorTestCase.test")) + +        self.assertGreater( +            len(error_results), +            0, +            "At least one job or test error result should have been returned") + + +def _filter_error_results(events): +    # Filter out job result events. +    return [ +        event +        for event in events +        if event.get("event", None) in ["job_result", "test_result"] and +        event.get("status", None) == "error" +        ] + + +if __name__ == "__main__": +    unittest.main() diff --git a/packages/Python/lldbsuite/test_event/test/src/event_collector.py b/packages/Python/lldbsuite/test_event/test/src/event_collector.py new file mode 100644 index 000000000000..35d132066898 --- /dev/null +++ b/packages/Python/lldbsuite/test_event/test/src/event_collector.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import +from __future__ import print_function + +import os +import subprocess +import sys +import tempfile + +# noinspection PyUnresolvedReferences +from six.moves import cPickle + + +def path_to_dotest_py(): +    return os.path.join( +        os.path.dirname(__file__), +        os.path.pardir, +        os.path.pardir, +        os.path.pardir, +        os.path.pardir, +        os.path.pardir, +        os.path.pardir, +        "test", +        "dotest.py") + + +def _make_pickled_events_filename(): +    with tempfile.NamedTemporaryFile( +            prefix="lldb_test_event_pickled_event_output", +            delete=False) as temp_file: +        return temp_file.name + + +def _collect_events_with_command(command, events_filename): +    # Run the single test with dotest.py, outputting +    # the raw pickled events to a temp file. +    with open(os.devnull, 'w') as dev_null_file: +        subprocess.call( +            command, +            stdout=dev_null_file, +            stderr=dev_null_file) + +    # Unpickle the events +    events = [] +    if os.path.exists(events_filename): +        with open(events_filename, "rb") as events_file: +            while True: +                try: +                    # print("reading event") +                    event = cPickle.load(events_file) +                    # print("read event: {}".format(event)) +                    if event: +                        events.append(event) +                except EOFError: +                    # This is okay. +                    break +        os.remove(events_filename) +    return events + + +def collect_events_whole_file(test_filename): +    events_filename = _make_pickled_events_filename() +    command = [ +        sys.executable, +        path_to_dotest_py(), +        "--inferior", +        "--results-formatter=lldbsuite.test_event.formatter.pickled.RawPickledFormatter", +        "--results-file={}".format(events_filename), +        "-p", os.path.basename(test_filename), +        os.path.dirname(test_filename) +        ] +    return _collect_events_with_command(command, events_filename) + + +def collect_events_for_directory_with_filter(test_filename, filter_desc): +    events_filename = _make_pickled_events_filename() +    command = [ +        sys.executable, +        path_to_dotest_py(), +        "--inferior", +        "--results-formatter=lldbsuite.test_event.formatter.pickled.RawPickledFormatter", +        "--results-file={}".format(events_filename), +        "-f", filter_desc, +        os.path.dirname(test_filename) +        ] +    return _collect_events_with_command(command, events_filename)  | 
