Files
mongo/buildscripts/resmokelib/testing/report.py
Max Hirschhorn 424314f65e SERVER-1424 Rewrite smoke.py.
Split out the passthrough tests into separate suites. The MongoDB
deployment is started up by resmoke.py so that we can record the
success/failure of each individual test in MCI.

Added support for parallel execution of tests by dispatching to
multiple MongoDB deployments.

Added support for grouping different kinds of tests (e.g. C++ unit
tests, dbtests, and jstests) so that they can be run together. This
allows for customizability in specifying what tests to execute when
changes are made to a particular part of the code.
2015-05-08 14:49:42 -04:00

292 lines
10 KiB
Python

"""
Extension to the unittest.TestResult to support additional test status
and timing information for the report.json file.
"""
from __future__ import absolute_import
import time
import unittest
from .. import config
from .. import logging
class TestReport(unittest.TestResult):
"""
Records test status and timing information.
"""
def __init__(self, logger, logging_config, build_id=None, build_config=None):
"""
Initializes the TestReport with the buildlogger configuration.
"""
unittest.TestResult.__init__(self)
self.logger = logger
self.logging_config = logging_config
self.build_id = build_id
self.build_config = build_config
self.start_times = {}
self.end_times = {}
self.statuses = {}
self.return_codes = {}
self.urls = {}
self.num_succeeded = 0
self.num_failed = 0
self.num_errored = 0
self.__dynamic_tests = set()
self.__original_loggers = {}
@classmethod
def combine(cls, *reports):
"""
Merges the results from multiple TestReport instances into one.
If the same test is present in multiple reports, then one that
failed or errored is more preferred over one that succeeded.
This behavior is useful for when running multiple jobs that
dynamically add a #dbhash# test case.
"""
combined_report = cls(logging.loggers.EXECUTOR, {})
combining_time = time.time()
for report in reports:
if not isinstance(report, TestReport):
raise TypeError("reports must be a list of TestReport instances")
for test_id in report.start_times:
if combined_report.statuses.get(test_id, "pass") != "pass":
# 'combined_report' already has a failure recorded for this test, so just keep
# the information about that one.
continue
combined_report.start_times[test_id] = report.start_times[test_id]
combined_report.end_times[test_id] = report.end_times.get(test_id, combining_time)
# If a StopExecution exception is triggered while running the tests, then it is
# possible for dynamic tests not to have called TestReport.stopTest() yet.
if test_id in report.__dynamic_tests:
# Mark a dynamic test as having failed if it was interrupted. It might have
# passed if the suite ran to completion, but we wouldn't know for sure.
combined_report.statuses[test_id] = report.statuses.get(test_id, "fail")
combined_report.return_codes[test_id] = report.return_codes.get(test_id, -2)
else:
# A non-dynamic test should always have a status and return code, so it is a
# resmoke.py error if it does not.
combined_report.statuses[test_id] = report.statuses.get(test_id, "error")
combined_report.return_codes[test_id] = report.return_codes.get(test_id, 2)
if test_id in report.urls:
combined_report.urls[test_id] = report.urls[test_id]
combined_report.__dynamic_tests.update(report.__dynamic_tests)
# Recompute number of success, failures, and errors.
combined_report.num_succeeded = len(combined_report.get_successful())
combined_report.num_failed = len(combined_report.get_failed())
combined_report.num_errored = len(combined_report.get_errored())
return combined_report
def startTest(self, test, dynamic=False):
"""
Called immediately before 'test' is run.
"""
unittest.TestResult.startTest(self, test)
self.start_times[test.id()] = time.time()
basename = test.basename()
if dynamic:
command = "(dynamic test case)"
self.__dynamic_tests.add(test.id())
else:
command = test.as_command()
self.logger.info("Running %s...\n%s", basename, command)
test_id = logging.buildlogger.new_test_id(self.build_id,
self.build_config,
basename,
command)
if self.build_id is not None:
endpoint = logging.buildlogger.APPEND_TEST_LOGS_ENDPOINT % {
"build_id": self.build_id,
"test_id": test_id,
}
self.urls[test.id()] = "%s/%s/" % (config.BUILDLOGGER_URL.rstrip("/"),
endpoint.strip("/"))
self.logger.info("Writing output of %s to %s.",
test.shortDescription(), self.urls[test.id()])
# Set up the test-specific logger.
logger_name = "%s:%s" % (test.logger.name, test.short_name())
logger = logging.loggers.new_logger(logger_name, parent=test.logger)
logging.config.apply_buildlogger_test_handler(logger,
self.logging_config,
build_id=self.build_id,
build_config=self.build_config,
test_id=test_id)
self.__original_loggers[test.id()] = test.logger
test.logger = logger
def stopTest(self, test):
"""
Called immediately after 'test' has run.
"""
unittest.TestResult.stopTest(self, test)
self.end_times[test.id()] = time.time()
time_taken = self.end_times[test.id()] - self.start_times[test.id()]
self.logger.info("%s ran in %0.2f seconds.", test.basename(), time_taken)
# Asynchronously closes the buildlogger test handler to avoid having too many threads open
# on 32-bit systems.
logging.flush.close_later(test.logger)
# Restore the original logger for the test.
test.logger = self.__original_loggers.pop(test.id())
def addError(self, test, err):
"""
Called when a non-failureException was raised during the
execution of 'test'.
"""
unittest.TestResult.addError(self, test, err)
self.num_errored += 1
self.statuses[test.id()] = "error"
self.return_codes[test.id()] = test.return_code
def setError(self, test):
"""
Used to change the outcome of an existing test to an error.
"""
if test.id() not in self.start_times or test.id() not in self.end_times:
raise ValueError("setError called on a test that has not completed.")
self.statuses[test.id()] = "error"
self.return_codes[test.id()] = 2
# Recompute number of success, failures, and errors.
self.num_succeeded = len(self.get_successful())
self.num_failed = len(self.get_failed())
self.num_errored = len(self.get_errored())
def addFailure(self, test, err):
"""
Called when a failureException was raised during the execution
of 'test'.
"""
unittest.TestResult.addFailure(self, test, err)
self.num_failed += 1
self.statuses[test.id()] = "fail"
self.return_codes[test.id()] = test.return_code
def setFailure(self, test, return_code=1):
"""
Used to change the outcome of an existing test to a failure.
"""
if test.id() not in self.start_times or test.id() not in self.end_times:
raise ValueError("setFailure called on a test that has not completed.")
self.statuses[test.id()] = "fail"
self.return_codes[test.id()] = return_code
# Recompute number of success, failures, and errors.
self.num_succeeded = len(self.get_successful())
self.num_failed = len(self.get_failed())
self.num_errored = len(self.get_errored())
def addSuccess(self, test):
"""
Called when 'test' executed successfully.
"""
unittest.TestResult.addSuccess(self, test)
self.num_succeeded += 1
self.statuses[test.id()] = "pass"
self.return_codes[test.id()] = test.return_code
def wasSuccessful(self):
"""
Returns true if all tests executed successfully.
"""
return self.num_failed == self.num_errored == 0
def num_dynamic(self):
"""
Returns the number of tests for which startTest(dynamic=True)
was called.
"""
return len(self.__dynamic_tests)
def get_successful(self):
"""
Returns the ids of the tests that executed successfully.
"""
return [test_id for test_id in self.statuses if self.statuses[test_id] == "pass"]
def get_failed(self):
"""
Returns the ids of the tests that raised a failureException
during their execution.
"""
return [test_id for test_id in self.statuses if self.statuses[test_id] == "fail"]
def get_errored(self):
"""
Returns the ids of the tests that raised a non-failureException
during their execution.
"""
return [test_id for test_id in self.statuses if self.statuses[test_id] == "error"]
def as_dict(self):
"""
Return the test result information as a dictionary.
Used to create the report.json file.
"""
results = []
for test_id in self.start_times:
# Don't distinguish between failures and errors.
status = "pass" if self.statuses[test_id] == "pass" else "fail"
start_time = self.start_times[test_id]
end_time = self.end_times[test_id]
result = {
"test_file": test_id,
"status": status,
"start": start_time,
"end": end_time,
"elapsed": end_time - start_time,
}
return_code = self.return_codes[test_id]
if return_code is not None:
result["exit_code"] = return_code
if test_id in self.urls:
result["url"] = self.urls[test_id]
results.append(result)
return {
"results": results,
"failures": self.num_failed + self.num_errored,
}