Files
mongo/buildscripts/resmokelib/testing/testcases.py

486 lines
16 KiB
Python

"""
Subclasses of unittest.TestCase.
"""
from __future__ import absolute_import
import os
import os.path
import shutil
import threading
import unittest
from .. import config
from .. import core
from .. import logging
from .. import utils
def make_test_case(test_kind, *args, **kwargs):
"""
Factory function for creating TestCase instances.
"""
if test_kind not in _TEST_CASES:
raise ValueError("Unknown test kind '%s'" % (test_kind))
return _TEST_CASES[test_kind](*args, **kwargs)
class TestCase(unittest.TestCase):
"""
A test case to execute.
"""
def __init__(self, logger, test_kind, test_name):
"""
Initializes the TestCase with the name of the test.
"""
unittest.TestCase.__init__(self, methodName="run_test")
if not isinstance(logger, logging.Logger):
raise TypeError("logger must be a Logger instance")
if not isinstance(test_kind, basestring):
raise TypeError("test_kind must be a string")
if not isinstance(test_name, basestring):
raise TypeError("test_name must be a string")
self.logger = logger
self.test_kind = test_kind
self.test_name = test_name
self.fixture = None
self.return_code = None
self.is_configured = False
def long_name(self):
"""
Returns the path to the test, relative to the current working directory.
"""
return os.path.relpath(self.test_name)
def basename(self):
"""
Returns the basename of the test.
"""
return os.path.basename(self.test_name)
def short_name(self):
"""
Returns the basename of the test without the file extension.
"""
return os.path.splitext(self.basename())[0]
def id(self):
return self.test_name
def shortDescription(self):
return "%s %s" % (self.test_kind, self.test_name)
def configure(self, fixture, *args, **kwargs):
"""
Stores 'fixture' as an attribute for later use during execution.
"""
if self.is_configured:
raise RuntimeError("configure can only be called once")
self.is_configured = True
self.fixture = fixture
def run_test(self):
"""
Runs the specified test.
"""
raise NotImplementedError("run_test must be implemented by TestCase subclasses")
def as_command(self):
"""
Returns the command invocation used to run the test.
"""
return self._make_process().as_command()
def _execute(self, process):
"""
Runs the specified process.
"""
if config.INTERNAL_EXECUTOR_NAME is not None:
self.logger.info("Starting %s under executor %s...\n%s",
self.shortDescription(),
config.INTERNAL_EXECUTOR_NAME,
process.as_command())
else:
self.logger.info("Starting %s...\n%s", self.shortDescription(), process.as_command())
process.start()
self.logger.info("%s started with pid %s.", self.shortDescription(), process.pid)
self.return_code = process.wait()
if self.return_code != 0:
raise self.failureException("%s failed" % (self.shortDescription()))
self.logger.info("%s finished.", self.shortDescription())
def _make_process(self):
"""
Returns a new Process instance that could be used to run the
test or log the command.
"""
raise NotImplementedError("_make_process must be implemented by TestCase subclasses")
class CPPUnitTestCase(TestCase):
"""
A C++ unit test to execute.
"""
def __init__(self,
logger,
program_executable,
program_options=None):
"""
Initializes the CPPUnitTestCase with the executable to run.
"""
TestCase.__init__(self, logger, "Program", program_executable)
self.program_executable = program_executable
self.program_options = utils.default_if_none(program_options, {}).copy()
def run_test(self):
try:
program = self._make_process()
self._execute(program)
except self.failureException:
raise
except:
self.logger.exception("Encountered an error running C++ unit test %s.", self.basename())
raise
def _make_process(self):
return core.process.Process(self.logger,
[self.program_executable],
**self.program_options)
class CPPIntegrationTestCase(TestCase):
"""
A C++ integration test to execute.
"""
def __init__(self,
logger,
program_executable,
program_options=None):
"""
Initializes the CPPIntegrationTestCase with the executable to run.
"""
TestCase.__init__(self, logger, "Program", program_executable)
self.program_executable = program_executable
self.program_options = utils.default_if_none(program_options, {}).copy()
def configure(self, fixture, *args, **kwargs):
TestCase.configure(self, fixture, *args, **kwargs)
self.program_options["connectionString"] = self.fixture.get_connection_string()
def run_test(self):
try:
program = self._make_process()
self._execute(program)
except self.failureException:
raise
except:
self.logger.exception("Encountered an error running C++ integration test %s.",
self.basename())
raise
def _make_process(self):
return core.programs.generic_program(self.logger,
[self.program_executable],
**self.program_options)
class DBTestCase(TestCase):
"""
A dbtest to execute.
"""
def __init__(self,
logger,
dbtest_suite,
dbtest_executable=None,
dbtest_options=None):
"""
Initializes the DBTestCase with the dbtest suite to run.
"""
TestCase.__init__(self, logger, "DBTest", dbtest_suite)
# Command line options override the YAML configuration.
self.dbtest_executable = utils.default_if_none(config.DBTEST_EXECUTABLE, dbtest_executable)
self.dbtest_suite = dbtest_suite
self.dbtest_options = utils.default_if_none(dbtest_options, {}).copy()
def configure(self, fixture, *args, **kwargs):
TestCase.configure(self, fixture, *args, **kwargs)
# If a dbpath was specified, then use it as a container for all other dbpaths.
dbpath_prefix = self.dbtest_options.pop("dbpath", DBTestCase._get_dbpath_prefix())
dbpath = os.path.join(dbpath_prefix, "job%d" % (self.fixture.job_num), "unittest")
self.dbtest_options["dbpath"] = dbpath
shutil.rmtree(dbpath, ignore_errors=True)
try:
os.makedirs(dbpath)
except os.error:
# Directory already exists.
pass
def run_test(self):
try:
dbtest = self._make_process()
self._execute(dbtest)
except self.failureException:
raise
except:
self.logger.exception("Encountered an error running dbtest suite %s.", self.basename())
raise
def _make_process(self):
return core.programs.dbtest_program(self.logger,
executable=self.dbtest_executable,
suites=[self.dbtest_suite],
**self.dbtest_options)
@staticmethod
def _get_dbpath_prefix():
"""
Returns the prefix of the dbpath to use for the dbtest
executable.
Order of preference:
1. The --dbpathPrefix specified at the command line.
2. Value of the TMPDIR environment variable.
3. Value of the TEMP environment variable.
4. Value of the TMP environment variable.
5. The /tmp directory.
"""
if config.DBPATH_PREFIX is not None:
return config.DBPATH_PREFIX
for env_var in ("TMPDIR", "TEMP", "TMP"):
if env_var in os.environ:
return os.environ[env_var]
return os.path.normpath("/tmp")
class JSTestCase(TestCase):
"""
A jstest to execute.
"""
# A wrapper for the thread class that lets us propagate exceptions.
class ExceptionThread(threading.Thread):
def __init__(self, my_target, my_args):
threading.Thread.__init__(self, target=my_target, args=my_args)
self.err = None
def run(self):
try:
threading.Thread.run(self)
except Exception as self.err:
raise
else:
self.err = None
def _get_exception(self):
return self.err
DEFAULT_CLIENT_NUM = 1
def __init__(self,
logger,
js_filename,
shell_executable=None,
shell_options=None,
test_kind="JSTest"):
"Initializes the JSTestCase with the JS file to run."
TestCase.__init__(self, logger, test_kind, js_filename)
# Command line options override the YAML configuration.
self.shell_executable = utils.default_if_none(config.MONGO_EXECUTABLE, shell_executable)
self.js_filename = js_filename
self.shell_options = utils.default_if_none(shell_options, {}).copy()
self.num_clients = JSTestCase.DEFAULT_CLIENT_NUM
def configure(self, fixture, num_clients=DEFAULT_CLIENT_NUM, *args, **kwargs):
TestCase.configure(self, fixture, *args, **kwargs)
if self.fixture.port is not None:
self.shell_options["port"] = self.fixture.port
global_vars = self.shell_options.get("global_vars", {}).copy()
data_dir = self._get_data_dir(global_vars)
# Set MongoRunner.dataPath if overridden at command line or not specified in YAML.
if config.DBPATH_PREFIX is not None or "MongoRunner.dataPath" not in global_vars:
# dataPath property is the dataDir property with a trailing slash.
data_path = os.path.join(data_dir, "")
else:
data_path = global_vars["MongoRunner.dataPath"]
global_vars["MongoRunner.dataDir"] = data_dir
global_vars["MongoRunner.dataPath"] = data_path
# Don't set the path to the executables when the user didn't specify them via the command
# line. The functions in the mongo shell for spawning processes have their own logic for
# determining the default path to use.
if config.MONGOD_EXECUTABLE is not None:
global_vars["MongoRunner.mongodPath"] = config.MONGOD_EXECUTABLE
if config.MONGOS_EXECUTABLE is not None:
global_vars["MongoRunner.mongosPath"] = config.MONGOS_EXECUTABLE
if self.shell_executable is not None:
global_vars["MongoRunner.mongoShellPath"] = self.shell_executable
test_data = global_vars.get("TestData", {}).copy()
test_data["minPort"] = core.network.PortAllocator.min_test_port(fixture.job_num)
test_data["maxPort"] = core.network.PortAllocator.max_test_port(fixture.job_num)
# Marks the main test when multiple test clients are run concurrently, to notify the test
# of any code that should only be run once. If there is only one client, it is the main one.
test_data["isMainTest"] = True
global_vars["TestData"] = test_data
self.shell_options["global_vars"] = global_vars
shutil.rmtree(data_dir, ignore_errors=True)
self.num_clients = num_clients
try:
os.makedirs(data_dir)
except os.error:
# Directory already exists.
pass
def _get_data_dir(self, global_vars):
"""
Returns the value that the mongo shell should set for the
MongoRunner.dataDir property.
"""
# Command line options override the YAML configuration.
data_dir_prefix = utils.default_if_none(config.DBPATH_PREFIX,
global_vars.get("MongoRunner.dataDir"))
data_dir_prefix = utils.default_if_none(data_dir_prefix, config.DEFAULT_DBPATH_PREFIX)
return os.path.join(data_dir_prefix,
"job%d" % (self.fixture.job_num),
config.MONGO_RUNNER_SUBDIR)
def run_test(self):
threads = []
try:
# Don't thread if there is only one client.
if self.num_clients == 1:
shell = self._make_process(self.logger)
self._execute(shell)
else:
# If there are multiple clients, make a new thread for each client.
for i in xrange(self.num_clients):
t = self.ExceptionThread(my_target=self._run_test_in_thread, my_args=[i])
t.start()
threads.append(t)
except self.failureException:
raise
except:
self.logger.exception("Encountered an error running jstest %s.", self.basename())
raise
finally:
for t in threads:
t.join()
for t in threads:
if t._get_exception() is not None:
raise t._get_exception()
def _make_process(self, logger=None, thread_id=0):
# If logger is none, it means that it's not running in a thread and thus logger should be
# set to self.logger.
logger = utils.default_if_none(logger, self.logger)
is_main_test = True
if thread_id > 0:
is_main_test = False
return core.programs.mongo_shell_program(logger,
executable=self.shell_executable,
filename=self.js_filename,
isMainTest=is_main_test,
**self.shell_options)
def _run_test_in_thread(self, thread_id):
# Make a logger for each thread.
logger = logging.loggers.new_logger(self.test_kind + ':' + str(thread_id),
parent=self.logger)
shell = self._make_process(logger, thread_id)
self._execute(shell)
class MongosTestCase(TestCase):
"""
A TestCase which runs a mongos binary with the given parameters.
"""
def __init__(self,
logger,
mongos_options):
"""
Initializes the mongos test and saves the options.
"""
self.mongos_executable = utils.default_if_none(config.MONGOS_EXECUTABLE,
config.DEFAULT_MONGOS_EXECUTABLE)
# Use the executable as the test name.
TestCase.__init__(self, logger, "mongos", self.mongos_executable)
self.options = mongos_options.copy()
def configure(self, fixture, *args, **kwargs):
"""
Ensures the --test option is present in the mongos options.
"""
TestCase.configure(self, fixture, *args, **kwargs)
# Always specify test option to ensure the mongos will terminate.
if "test" not in self.options:
self.options["test"] = ""
def run_test(self):
try:
mongos = self._make_process()
self._execute(mongos)
except self.failureException:
raise
except:
self.logger.exception("Encountered an error running %s.", mongos.as_command())
raise
def _make_process(self):
return core.programs.mongos_program(self.logger,
executable=self.mongos_executable,
**self.options)
_TEST_CASES = {
"cpp_unit_test": CPPUnitTestCase,
"cpp_integration_test": CPPIntegrationTestCase,
"db_test": DBTestCase,
"js_test": JSTestCase,
"mongos_test": MongosTestCase,
}