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
This commit is contained in:
Todd Fiala 2016-04-20 16:27:27 +00:00
parent e8fc69d136
commit 49d3c15c3e
14 changed files with 734 additions and 674 deletions

View File

@ -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)

View File

@ -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 "

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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>, 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
"""
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>, 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)

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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>, 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
"""
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>, 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 = "<no_filename>"
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")

View File

@ -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=("<unexpected-success />"))
inner_content="<unexpected-success />")
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.
"""