mirror of https://github.com/mongodb/mongo
227 lines
7.8 KiB
Python
227 lines
7.8 KiB
Python
"""
|
|
Enables supports for running tests simultaneously by processing them
|
|
from a multi-consumer queue.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import sys
|
|
|
|
from .. import config
|
|
from .. import errors
|
|
from ..utils import queue as _queue
|
|
|
|
|
|
class Job(object):
|
|
"""
|
|
Runs tests from a queue.
|
|
"""
|
|
|
|
def __init__(self, logger, fixture, hooks, report, archival, suite_options):
|
|
"""
|
|
Initializes the job with the specified fixture and hooks.
|
|
"""
|
|
|
|
self.logger = logger
|
|
self.fixture = fixture
|
|
self.hooks = hooks
|
|
self.report = report
|
|
self.archival = archival
|
|
self.suite_options = suite_options
|
|
|
|
def __call__(self, queue, interrupt_flag, teardown_flag=None):
|
|
"""
|
|
Continuously executes tests from 'queue' and records their
|
|
details in 'report'.
|
|
|
|
If 'teardown_flag' is not None, then 'self.fixture.teardown()'
|
|
will be called before this method returns. If an error occurs
|
|
while destroying the fixture, then the 'teardown_flag' will be
|
|
set.
|
|
"""
|
|
|
|
should_stop = False
|
|
try:
|
|
self._run(queue, interrupt_flag)
|
|
except errors.StopExecution as err:
|
|
# Stop running tests immediately.
|
|
self.logger.error("Received a StopExecution exception: %s.", err)
|
|
should_stop = True
|
|
except:
|
|
# Unknown error, stop execution.
|
|
self.logger.exception("Encountered an error during test execution.")
|
|
should_stop = True
|
|
|
|
if should_stop:
|
|
# Set the interrupt flag so that other jobs do not start running more tests.
|
|
interrupt_flag.set()
|
|
# Drain the queue to unblock the main thread.
|
|
Job._drain_queue(queue)
|
|
|
|
if teardown_flag is not None:
|
|
try:
|
|
self.fixture.teardown(finished=True)
|
|
except errors.ServerFailure as err:
|
|
self.logger.warn("Teardown of %s was not successful: %s", self.fixture, err)
|
|
teardown_flag.set()
|
|
except:
|
|
self.logger.exception("Encountered an error while tearing down %s.", self.fixture)
|
|
teardown_flag.set()
|
|
|
|
def _run(self, queue, interrupt_flag):
|
|
"""
|
|
Calls the before/after suite hooks and continuously executes
|
|
tests from 'queue'.
|
|
"""
|
|
|
|
for hook in self.hooks:
|
|
hook.before_suite(self.report)
|
|
|
|
while not interrupt_flag.is_set():
|
|
test = queue.get_nowait()
|
|
try:
|
|
if test is None:
|
|
# Sentinel value received, so exit.
|
|
break
|
|
self._execute_test(test)
|
|
finally:
|
|
queue.task_done()
|
|
|
|
for hook in self.hooks:
|
|
hook.after_suite(self.report)
|
|
|
|
def _execute_test(self, test):
|
|
"""
|
|
Calls the before/after test hooks and executes 'test'.
|
|
"""
|
|
|
|
test.configure(self.fixture, config.NUM_CLIENTS_PER_FIXTURE)
|
|
self._run_hooks_before_tests(test)
|
|
|
|
test(self.report)
|
|
try:
|
|
if self.suite_options.fail_fast and not self.report.wasSuccessful():
|
|
self.logger.info("%s failed, so stopping..." % (test.shortDescription()))
|
|
raise errors.StopExecution("%s failed" % (test.shortDescription()))
|
|
|
|
if not self.fixture.is_running():
|
|
self.logger.error(
|
|
"%s marked as a failure because the fixture crashed during the test.",
|
|
test.shortDescription())
|
|
self.report.setFailure(test, return_code=2)
|
|
# Always fail fast if the fixture fails.
|
|
raise errors.StopExecution("%s not running after %s" % (self.fixture,
|
|
test.shortDescription()))
|
|
finally:
|
|
success = self.report._find_test_info(test).status == "pass"
|
|
if self.archival:
|
|
self.archival.archive(self.logger, test, success)
|
|
|
|
self._run_hooks_after_tests(test)
|
|
|
|
def _run_hook(self, hook, hook_function, test):
|
|
""" Helper to run hook and archival. """
|
|
try:
|
|
success = False
|
|
hook_function(test, self.report)
|
|
success = True
|
|
finally:
|
|
if self.archival:
|
|
self.archival.archive(self.logger, test, success, hook=hook)
|
|
|
|
def _run_hooks_before_tests(self, test):
|
|
"""
|
|
Runs the before_test method on each of the hooks.
|
|
|
|
Swallows any TestFailure exceptions if set to continue on
|
|
failure, and reraises any other exceptions.
|
|
"""
|
|
try:
|
|
for hook in self.hooks:
|
|
self._run_hook(hook, hook.before_test, test)
|
|
|
|
except errors.StopExecution:
|
|
raise
|
|
|
|
except errors.ServerFailure:
|
|
self.logger.exception("%s marked as a failure by a hook's before_test.",
|
|
test.shortDescription())
|
|
self._fail_test(test, sys.exc_info(), return_code=2)
|
|
raise errors.StopExecution("A hook's before_test failed")
|
|
|
|
except errors.TestFailure:
|
|
self.logger.exception("%s marked as a failure by a hook's before_test.",
|
|
test.shortDescription())
|
|
self._fail_test(test, sys.exc_info(), return_code=1)
|
|
if self.suite_options.fail_fast:
|
|
raise errors.StopExecution("A hook's before_test failed")
|
|
|
|
except:
|
|
# Record the before_test() error in 'self.report'.
|
|
self.report.startTest(test)
|
|
self.report.addError(test, sys.exc_info())
|
|
self.report.stopTest(test)
|
|
raise
|
|
|
|
def _run_hooks_after_tests(self, test):
|
|
"""
|
|
Runs the after_test method on each of the hooks.
|
|
|
|
Swallows any TestFailure exceptions if set to continue on
|
|
failure, and reraises any other exceptions.
|
|
"""
|
|
try:
|
|
for hook in self.hooks:
|
|
self._run_hook(hook, hook.after_test, test)
|
|
|
|
except errors.StopExecution:
|
|
raise
|
|
|
|
except errors.ServerFailure:
|
|
self.logger.exception("%s marked as a failure by a hook's after_test.",
|
|
test.shortDescription())
|
|
self.report.setFailure(test, return_code=2)
|
|
raise errors.StopExecution("A hook's after_test failed")
|
|
|
|
except errors.TestFailure:
|
|
self.logger.exception("%s marked as a failure by a hook's after_test.",
|
|
test.shortDescription())
|
|
self.report.setFailure(test, return_code=1)
|
|
if self.suite_options.fail_fast:
|
|
raise errors.StopExecution("A hook's after_test failed")
|
|
|
|
except:
|
|
self.report.setError(test)
|
|
raise
|
|
|
|
def _fail_test(self, test, exc_info, return_code=1):
|
|
"""
|
|
Helper to record a test as a failure with the provided return
|
|
code.
|
|
|
|
This method should not be used if 'test' has already been
|
|
started, instead use TestReport.setFailure().
|
|
"""
|
|
|
|
self.report.startTest(test)
|
|
test.return_code = return_code
|
|
self.report.addFailure(test, exc_info)
|
|
self.report.stopTest(test)
|
|
|
|
@staticmethod
|
|
def _drain_queue(queue):
|
|
"""
|
|
Removes all elements from 'queue' without actually doing
|
|
anything to them. Necessary to unblock the main thread that is
|
|
waiting for 'queue' to be empty.
|
|
"""
|
|
|
|
try:
|
|
while not queue.empty():
|
|
queue.get_nowait()
|
|
queue.task_done()
|
|
except _queue.Empty:
|
|
# Multiple threads may be draining the queue simultaneously, so just ignore the
|
|
# exception from the race between queue.empty() being false and failing to get an item.
|
|
pass
|