From 49d3c15c3e8cc01e2a8af096eeacff746d92c644 Mon Sep 17 00:00:00 2001 From: Todd Fiala Date: Wed, 20 Apr 2016 16:27:27 +0000 Subject: [PATCH] test infra: move test event-related handling into its own package This change moves all the test event handling and its related ResultsFormatter classes out of the packages/Python/lldbsuite/test dir into a packages/Python/lldbsuite/test_event package. Formatters are moved into a sub-package under that. I am limiting the scope of this change to just the motion and a few minor issues caught by a static Python checker (e.g. removing unused import statements). This is a pre-step for adding package-level tests to the test event system. I also intend to simplify test event results formatter selection after I make sure this doesn't break anybody. See: http://reviews.llvm.org/D19288 Reviewed by: Pavel Labath llvm-svn: 266885 --- lldb/packages/Python/lldbsuite/test/decorators.py | 7 +- lldb/packages/Python/lldbsuite/test/dosep.py | 15 +- lldb/packages/Python/lldbsuite/test/dotest.py | 18 +- lldb/packages/Python/lldbsuite/test/lldbtest.py | 7 +- lldb/packages/Python/lldbsuite/test/test_result.py | 3 +- .../Python/lldbsuite/test_event/__init__.py | 0 .../{test => test_event}/dotest_channels.py | 0 .../Python/lldbsuite/test_event/event_builder.py | 435 ++++++++++++++ .../lldbsuite/test_event/formatter/__init__.py | 160 ++++++ .../formatter/curses.py} | 19 +- .../test_event/formatter/dump_formatter.py | 23 + .../lldbsuite/test_event/formatter/pickled.py | 57 ++ .../formatter/results_formatter.py} | 635 +-------------------- .../formatter/xunit.py} | 29 +- 14 files changed, 734 insertions(+), 674 deletions(-) create mode 100644 lldb/packages/Python/lldbsuite/test_event/__init__.py rename lldb/packages/Python/lldbsuite/{test => test_event}/dotest_channels.py (100%) create mode 100644 lldb/packages/Python/lldbsuite/test_event/event_builder.py create mode 100644 lldb/packages/Python/lldbsuite/test_event/formatter/__init__.py rename lldb/packages/Python/lldbsuite/{test/curses_results.py => test_event/formatter/curses.py} (97%) create mode 100644 lldb/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py create mode 100644 lldb/packages/Python/lldbsuite/test_event/formatter/pickled.py rename lldb/packages/Python/lldbsuite/{test/result_formatter.py => test_event/formatter/results_formatter.py} (57%) rename lldb/packages/Python/lldbsuite/{test/xunit_formatter.py => test_event/formatter/xunit.py} (96%) diff --git a/lldb/packages/Python/lldbsuite/test/decorators.py b/lldb/packages/Python/lldbsuite/test/decorators.py index e73bfe3..84bce6e 100644 --- a/lldb/packages/Python/lldbsuite/test/decorators.py +++ b/lldb/packages/Python/lldbsuite/test/decorators.py @@ -1,10 +1,9 @@ -from __future__ import print_function from __future__ import absolute_import +from __future__ import print_function # System modules from distutils.version import LooseVersion, StrictVersion from functools import wraps -import itertools import os import re import sys @@ -19,7 +18,7 @@ import use_lldb_suite import lldb from . import configuration from . import test_categories -from .result_formatter import EventBuilder +from lldbsuite.test_event.event_builder import EventBuilder from lldbsuite.support import funcutils from lldbsuite.test import lldbplatform from lldbsuite.test import lldbplatformutil @@ -77,7 +76,6 @@ def expectedFailure(expected_fn, bugnumber=None): raise Exception("Decorator can only be used to decorate a test method") @wraps(func) def wrapper(*args, **kwargs): - from unittest2 import case self = args[0] if funcutils.requires_self(expected_fn): xfail_reason = expected_fn(self) @@ -110,7 +108,6 @@ def skipTestIfFn(expected_fn, bugnumber=None): @wraps(func) def wrapper(*args, **kwargs): - from unittest2 import case self = args[0] if funcutils.requires_self(expected_fn): reason = expected_fn(self) diff --git a/lldb/packages/Python/lldbsuite/test/dosep.py b/lldb/packages/Python/lldbsuite/test/dosep.py index e58bcaf..7d9dcd93 100644 --- a/lldb/packages/Python/lldbsuite/test/dosep.py +++ b/lldb/packages/Python/lldbsuite/test/dosep.py @@ -30,8 +30,8 @@ ulimit -c unlimited echo core.%p | sudo tee /proc/sys/kernel/core_pattern """ -from __future__ import print_function from __future__ import absolute_import +from __future__ import print_function # system packages and modules import asyncore @@ -52,13 +52,12 @@ from six.moves import queue import lldbsuite import lldbsuite.support.seven as seven -from lldbsuite.support import optional_with from . import configuration -from . import dotest_channels from . import dotest_args -from . import result_formatter - -from .result_formatter import EventBuilder +from lldbsuite.support import optional_with +from lldbsuite.test_event import dotest_channels +from lldbsuite.test_event.event_builder import EventBuilder +from lldbsuite.test_event import formatter from .test_runner import process_control @@ -299,9 +298,9 @@ def send_events_to_collector(events, command): event_port = int(command[arg_index]) # Create results formatter connected back to collector via socket. - config = result_formatter.FormatterConfig() + config = formatter.FormatterConfig() config.port = event_port - formatter_spec = result_formatter.create_results_formatter(config) + formatter_spec = formatter.create_results_formatter(config) if formatter_spec is None or formatter_spec.formatter is None: raise Exception( "Failed to create socket-based ResultsFormatter " diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py index 198e87c..fa164a6 100644 --- a/lldb/packages/Python/lldbsuite/test/dotest.py +++ b/lldb/packages/Python/lldbsuite/test/dotest.py @@ -23,7 +23,6 @@ from __future__ import print_function # System modules import atexit -import importlib import os import errno import platform @@ -31,7 +30,6 @@ import signal import socket import subprocess import sys -import inspect # Third-party modules import six @@ -43,9 +41,9 @@ from . import configuration from . import dotest_args from . import lldbtest_config from . import test_categories -from . import result_formatter +from lldbsuite.test_event import formatter from . import test_result -from .result_formatter import EventBuilder +from lldbsuite.test_event.event_builder import EventBuilder from ..support import seven def is_exe(fpath): @@ -359,7 +357,7 @@ def parseOptionsAndInitTestdirs(): # Capture test results-related args. if args.curses and not args.inferior: # Act as if the following args were set. - args.results_formatter = "lldbsuite.test.curses_results.Curses" + args.results_formatter = "lldbsuite.test_event.formatter.curses.Curses" args.results_file = "stdout" if args.results_file: @@ -383,7 +381,7 @@ def parseOptionsAndInitTestdirs(): # and we're not a test inferior. if not args.inferior and configuration.results_formatter_name is None: configuration.results_formatter_name = ( - "lldbsuite.test.result_formatter.ResultsFormatter") + "lldbsuite.test_event.formatter.results_formatter.ResultsFormatter") # rerun-related arguments configuration.rerun_all_issues = args.rerun_all_issues @@ -412,7 +410,7 @@ def parseOptionsAndInitTestdirs(): # Tell the event builder to create all events with these # key/val pairs in them. if len(entries) > 0: - result_formatter.EventBuilder.add_entries_to_all_events(entries) + EventBuilder.add_entries_to_all_events(entries) # Gather all the dirs passed on the command line. if len(args.args) > 0: @@ -453,7 +451,7 @@ def createSocketToLocalPort(port): def setupTestResults(): """Sets up test results-related objects based on arg settings.""" # Setup the results formatter configuration. - formatter_config = result_formatter.FormatterConfig() + formatter_config = formatter.FormatterConfig() formatter_config.filename = configuration.results_filename formatter_config.formatter_name = configuration.results_formatter_name formatter_config.formatter_options = ( @@ -461,12 +459,12 @@ def setupTestResults(): formatter_config.port = configuration.results_port # Create the results formatter. - formatter_spec = result_formatter.create_results_formatter( + formatter_spec = formatter.create_results_formatter( formatter_config) if formatter_spec is not None and formatter_spec.formatter is not None: configuration.results_formatter_object = formatter_spec.formatter - # Send an intialize message to the formatter. + # Send an initialize message to the formatter. initialize_event = EventBuilder.bare_event("initialize") if isMultiprocessTestRunner(): if (configuration.test_runner_name is not None and diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index 196ee8fa..492e136 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -31,8 +31,8 @@ OK $ """ -from __future__ import print_function from __future__ import absolute_import +from __future__ import print_function # System modules import abc @@ -42,12 +42,13 @@ import gc import glob import inspect import io -import os, sys, traceback import os.path import re import signal from subprocess import * +import sys import time +import traceback import types # Third-party modules @@ -68,8 +69,6 @@ from . import test_categories from lldbsuite.support import encoded_file from lldbsuite.support import funcutils -from .result_formatter import EventBuilder - # dosep.py starts lots and lots of dotest instances # This option helps you find if two (or more) dotest instances are using the same # directory at the same time diff --git a/lldb/packages/Python/lldbsuite/test/test_result.py b/lldb/packages/Python/lldbsuite/test/test_result.py index 9665d67..949ebc8 100644 --- a/lldb/packages/Python/lldbsuite/test/test_result.py +++ b/lldb/packages/Python/lldbsuite/test/test_result.py @@ -18,9 +18,8 @@ import inspect import unittest2 # LLDB Modules -import lldbsuite from . import configuration -from .result_formatter import EventBuilder +from lldbsuite.test_event.event_builder import EventBuilder class LLDBTestResult(unittest2.TextTestResult): diff --git a/lldb/packages/Python/lldbsuite/test_event/__init__.py b/lldb/packages/Python/lldbsuite/test_event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lldb/packages/Python/lldbsuite/test/dotest_channels.py b/lldb/packages/Python/lldbsuite/test_event/dotest_channels.py similarity index 100% rename from lldb/packages/Python/lldbsuite/test/dotest_channels.py rename to lldb/packages/Python/lldbsuite/test_event/dotest_channels.py diff --git a/lldb/packages/Python/lldbsuite/test_event/event_builder.py b/lldb/packages/Python/lldbsuite/test_event/event_builder.py new file mode 100644 index 0000000..3c43f00 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test_event/event_builder.py @@ -0,0 +1,435 @@ +""" + 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 + + +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). + + @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). + + @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). + + @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). + + @return the event dictionary + """ + return EventBuilder._event_dictionary_issue( + test, EventBuilder.STATUS_ERROR, error_tuple) + + @staticmethod + def event_for_cleanup_error(test, error_tuple): + """Returns an event dictionary for a test that hit a test execution error + during the test cleanup phase. + + @param test a unittest.TestCase instance. + + @param error_tuple the error tuple as reported by the test runner. + This is of the form (type, error). + + @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_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/lldb/packages/Python/lldbsuite/test_event/formatter/__init__.py b/lldb/packages/Python/lldbsuite/test_event/formatter/__init__.py new file mode 100644 index 0000000..f00766c --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test_event/formatter/__init__.py @@ -0,0 +1,160 @@ +""" + 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 + + +SOCKET_ACK_BYTE_VALUE = b'*' # ASCII for chr(42) + + +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[0] != SOCKET_ACK_BYTE_VALUE): + 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 + + 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") + + # 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) + + 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/lldb/packages/Python/lldbsuite/test/curses_results.py b/lldb/packages/Python/lldbsuite/test_event/formatter/curses.py similarity index 97% rename from lldb/packages/Python/lldbsuite/test/curses_results.py rename to lldb/packages/Python/lldbsuite/test_event/formatter/curses.py index 9d480b9..8578259 100644 --- a/lldb/packages/Python/lldbsuite/test/curses_results.py +++ b/lldb/packages/Python/lldbsuite/test_event/formatter/curses.py @@ -1,16 +1,12 @@ -#!/usr/bin/env python - """ The LLVM Compiler Infrastructure This file is distributed under the University of Illinois Open Source License. See LICENSE.TXT for details. - -Configuration options for lldbtest.py set by dotest.py during initialization """ -from __future__ import print_function from __future__ import absolute_import +from __future__ import print_function # System modules import curses @@ -22,12 +18,13 @@ import time # Third-party modules # LLDB modules -from . import lldbcurses -from . import result_formatter -from .result_formatter import EventBuilder +from lldbsuite.test import lldbcurses + +from . import results_formatter +from ..event_builder import EventBuilder -class Curses(result_formatter.ResultsFormatter): +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): @@ -75,6 +72,10 @@ class Curses(result_formatter.ResultsFormatter): return 'S' elif status == EventBuilder.STATUS_ERROR: return 'E' + elif status == EventBuilder.STATUS_TIMEOUT: + return 'T' + elif status == EventBuilder.STATUS_EXPECTED_TIMEOUT: + return 't' else: return status diff --git a/lldb/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py b/lldb/packages/Python/lldbsuite/test_event/formatter/dump_formatter.py new file mode 100644 index 0000000..d42dcb1 --- /dev/null +++ b/lldb/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/lldb/packages/Python/lldbsuite/test_event/formatter/pickled.py b/lldb/packages/Python/lldbsuite/test_event/formatter/pickled.py new file mode 100644 index 0000000..9607e91 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test_event/formatter/pickled.py @@ -0,0 +1,57 @@ +""" + 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 + + def __init__(self, out_file, options): + super(RawPickledFormatter, self).__init__(out_file, options) + self.pid = os.getpid() + + def handle_event(self, test_event): + super(RawPickledFormatter, self).handle_event(test_event) + + # Convert initialize/terminate events into job_begin/job_end events. + event_type = test_event["event"] + if event_type is None: + return + + if event_type == "initialize": + test_event["event"] = "job_begin" + elif event_type == "terminate": + test_event["event"] = "job_end" + + # Tack on the pid. + test_event["pid"] = self.pid + + # Send it as {serialized_length_of_serialized_bytes}{serialized_bytes} + import struct + msg = cPickle.dumps(test_event) + packet = struct.pack("!I%ds" % len(msg), len(msg), msg) + self.out_file.send(packet) diff --git a/lldb/packages/Python/lldbsuite/test/result_formatter.py b/lldb/packages/Python/lldbsuite/test_event/formatter/results_formatter.py similarity index 57% rename from lldb/packages/Python/lldbsuite/test/result_formatter.py rename to lldb/packages/Python/lldbsuite/test_event/formatter/results_formatter.py index 4ad39db..0fc5649 100644 --- a/lldb/packages/Python/lldbsuite/test/result_formatter.py +++ b/lldb/packages/Python/lldbsuite/test_event/formatter/results_formatter.py @@ -13,587 +13,24 @@ from __future__ import absolute_import # System modules import argparse -import importlib -import inspect import os -import pprint -import socket import sys import threading -import time -import traceback # Third-party modules -import six -from six.moves import cPickle + # LLDB modules -from . import configuration +from lldbsuite.test import configuration +from ..event_builder import EventBuilder import lldbsuite -# 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) - # print("\n** socket creation: received ack: {}".format(ord(read_bytes[0])), file=sys.stderr) - - return (sock, lambda: socket_closer(sock)) - - default_formatter_name = None - results_file_object = None - cleanup_func = None - - 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.xunit_formatter.XunitFormatter") - elif config.port: - # Connect to the specified localhost port. - results_file_object, cleanup_func = create_socket(config.port) - default_formatter_name = ( - "lldbsuite.test.result_formatter.RawPickledFormatter") - - # 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) - - 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 - - -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 = set([ - 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 = set([ - 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). - - @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). - - @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). - - @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). - - @return the event dictionary - """ - return EventBuilder._event_dictionary_issue( - test, EventBuilder.STATUS_ERROR, error_tuple) - - @staticmethod - def event_for_cleanup_error(test, error_tuple): - """Returns an event dictionary for a test that hit a test execution error - during the test cleanup phase. - - @param test a unittest.TestCase instance. - - @param error_tuple the error tuple as reported by the test runner. - This is of the form (type, error). - - @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_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 rerunnable - 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) - - class ResultsFormatter(object): """Provides interface to formatting test results out to a file-like object. - This class allows the LLDB test framework's raw test-realted + 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. @@ -609,7 +46,7 @@ class ResultsFormatter(object): # passed to dotest.py via the "--results-formatter-options" # argument. See the help on that for syntactic requirements # on getting that parsed correctly. - formatter = SomeResultFormatter(file_like_object, argpared_options_dict) + formatter = SomeResultFormatter(file_like_object, argparse_options_dict) # Single call to session start, before parsing any events. formatter.begin_session() @@ -625,7 +62,7 @@ class ResultsFormatter(object): for event in zero_or_more_test_events(): formatter.handle_event(event) - # Single call to terminate/wrap-up. Formatters that need all the + # 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",...}) @@ -755,6 +192,8 @@ class ResultsFormatter(object): if "test_filename" in result_event: key = result_event["test_filename"] component_count += 1 + else: + key = "" if "test_class" in result_event: if component_count > 0: key += ":" @@ -1010,6 +449,7 @@ class ResultsFormatter(object): # 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. @@ -1070,7 +510,8 @@ class ResultsFormatter(object): key=lambda x: self._event_sort_key(x[1])) return partitioned_events - def _print_banner(self, out_file, banner_text): + @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. @@ -1161,7 +602,8 @@ class ResultsFormatter(object): # details. return False - def _report_category_details(self, out_file, category, result_events_by_status): + @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. @@ -1266,58 +708,9 @@ class ResultsFormatter(object): if self.options.dump_results: # Debug dump of the key/result info for all categories. - self._print_banner("Results Dump") + 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)) - - -class RawPickledFormatter(ResultsFormatter): - """Formats events as a pickled stream. - - The parallel test runner has inferiors pickle their results and send them - over a socket back to the parallel test. The parallel test runner then - aggregates them into the final results formatter (e.g. xUnit). - """ - - @classmethod - def arg_parser(cls): - """@return arg parser used to parse formatter-specific options.""" - parser = super(RawPickledFormatter, cls).arg_parser() - return parser - - def __init__(self, out_file, options): - super(RawPickledFormatter, self).__init__(out_file, options) - self.pid = os.getpid() - - def handle_event(self, test_event): - super(RawPickledFormatter, self).handle_event(test_event) - - # Convert initialize/terminate events into job_begin/job_end events. - event_type = test_event["event"] - if event_type is None: - return - - if event_type == "initialize": - test_event["event"] = "job_begin" - elif event_type == "terminate": - test_event["event"] = "job_end" - - # Tack on the pid. - test_event["pid"] = self.pid - - # Send it as {serialized_length_of_serialized_bytes}{serialized_bytes} - import struct - msg = cPickle.dumps(test_event) - packet = struct.pack("!I%ds" % len(msg), len(msg), msg) - self.out_file.send(packet) - - -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/lldb/packages/Python/lldbsuite/test/xunit_formatter.py b/lldb/packages/Python/lldbsuite/test_event/formatter/xunit.py similarity index 96% rename from lldb/packages/Python/lldbsuite/test/xunit_formatter.py rename to lldb/packages/Python/lldbsuite/test_event/formatter/xunit.py index 41eac97..02a49ed 100644 --- a/lldb/packages/Python/lldbsuite/test/xunit_formatter.py +++ b/lldb/packages/Python/lldbsuite/test_event/formatter/xunit.py @@ -8,8 +8,8 @@ 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 print_function from __future__ import absolute_import +from __future__ import print_function # System modules import re @@ -20,8 +20,8 @@ import xml.sax.saxutils import six # Local modules -from .result_formatter import EventBuilder -from .result_formatter import ResultsFormatter +from ..event_builder import EventBuilder +from .results_formatter import ResultsFormatter class XunitFormatter(ResultsFormatter): @@ -36,10 +36,10 @@ class XunitFormatter(ResultsFormatter): @staticmethod def _build_illegal_xml_regex(): - """Contructs a regex to match all illegal xml characters. + """Constructs a regex to match all illegal xml characters. Expects to be used against a unicode string.""" - # Construct the range pairs of invalid unicode chareacters. + # 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)] @@ -139,10 +139,10 @@ class XunitFormatter(ResultsFormatter): @staticmethod def _build_regex_list_from_patterns(patterns): - """Builds a list of compiled regexes from option value. + """Builds a list of compiled regular expressions from option value. - @param option string containing a comma-separated list of regex - patterns. Zero-length or None will produce an empty regex list. + @param patterns contains a list of regular expression + patterns. @return list of compiled regular expressions, empty if no patterns provided. @@ -156,7 +156,7 @@ class XunitFormatter(ResultsFormatter): def __init__(self, out_file, options): """Initializes the XunitFormatter instance. @param out_file file-like object where formatted output is written. - @param options_dict specifies a dictionary of options for the + @param options specifies a dictionary of options for the formatter. """ # Initialize the parent @@ -198,9 +198,7 @@ class XunitFormatter(ResultsFormatter): self._handle_timeout } - RESULT_TYPES = set( - [EventBuilder.TYPE_TEST_RESULT, - EventBuilder.TYPE_JOB_RESULT]) + RESULT_TYPES = {EventBuilder.TYPE_TEST_RESULT, EventBuilder.TYPE_JOB_RESULT} def handle_event(self, test_event): super(XunitFormatter, self).handle_event(test_event) @@ -401,7 +399,8 @@ class XunitFormatter(ResultsFormatter): raise Exception( "unknown xfail option: {}".format(self.options.xfail)) - def _handle_expected_timeout(self, test_event): + @staticmethod + def _handle_expected_timeout(test_event): """Handles expected_timeout. @param test_event the test event to handle. """ @@ -418,7 +417,7 @@ class XunitFormatter(ResultsFormatter): # test results viewer. result = self._common_add_testcase_entry( test_event, - inner_content=("")) + inner_content="") with self.lock: self.elements["unexpected_successes"].append(result) elif self.options.xpass == XunitFormatter.RM_SUCCESS: @@ -519,7 +518,7 @@ class XunitFormatter(ResultsFormatter): xUnit output is in XML. The reporting system cannot complete the formatting of the output without knowing when there is no more input. - This call addresses notifcation of the completed test run and thus is + This call addresses notification of the completed test run and thus is when we can finish off the report output. """ -- 2.7.4