""" unittest.TestCase for JavaScript tests. """ from __future__ import absolute_import import os import os.path import shutil import sys import threading from . import interface from ... import config from ... import core from ... import utils from ...utils import registry class _SingleJSTestCase(interface.ProcessTestCase): """ A jstest to execute. """ REGISTERED_NAME = registry.LEAVE_UNREGISTERED def __init__(self, logger, js_filename, shell_executable=None, shell_options=None): """ Initializes the _SingleJSTestCase with the JS file to run. """ interface.ProcessTestCase.__init__(self, logger, "JSTest", 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() def configure(self, fixture, *args, **kwargs): interface.ProcessTestCase.configure(self, fixture, *args, **kwargs) def configure_shell(self): """ Sets up the global variables for the shell, and data/ directory for the mongod. configure_shell() only needs to be called once per test. Therefore if creating multiple _SingleJSTestCase instances to be run in parallel, only call configure_shell() on one of them. """ 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(self.fixture.job_num) test_data["maxPort"] = core.network.PortAllocator.max_test_port(self.fixture.job_num) test_data["failIfUnterminatedProcesses"] = True global_vars["TestData"] = test_data self.shell_options["global_vars"] = global_vars shutil.rmtree(data_dir, ignore_errors=True) try: os.makedirs(data_dir) except os.error: # Directory already exists. pass process_kwargs = self.shell_options.get("process_kwargs", {}).copy() if "KRB5_CONFIG" in process_kwargs and "KRB5CCNAME" not in process_kwargs: # Use a job-specific credential cache for JavaScript tests involving Kerberos. krb5_dir = os.path.join(data_dir, "krb5") try: os.makedirs(krb5_dir) except os.error: pass process_kwargs["KRB5CCNAME"] = "DIR:" + os.path.join(krb5_dir, ".") self.shell_options["process_kwargs"] = process_kwargs 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 _make_process(self): return core.programs.mongo_shell_program( self.logger, executable=self.shell_executable, filename=self.js_filename, connection_string=self.fixture.get_driver_connection_url(), **self.shell_options) class JSTestCase(interface.ProcessTestCase): """ A wrapper for several copies of a SingleJSTest to execute. """ REGISTERED_NAME = "js_test" class ThreadWithException(threading.Thread): """ A wrapper for the thread class that lets us propagate exceptions. """ def __init__(self, *args, **kwargs): threading.Thread.__init__(self, *args, **kwargs) self.exc_info = None def run(self): try: threading.Thread.run(self) except: self.exc_info = sys.exc_info() DEFAULT_CLIENT_NUM = 1 def __init__(self, logger, js_filename, shell_executable=None, shell_options=None): """ Initializes the JSTestCase with the JS file to run. """ interface.ProcessTestCase.__init__(self, logger, "JSTest", js_filename) self.num_clients = JSTestCase.DEFAULT_CLIENT_NUM self.test_case_template = _SingleJSTestCase(logger, js_filename, shell_executable, shell_options) def configure(self, fixture, num_clients=DEFAULT_CLIENT_NUM, *args, **kwargs): interface.ProcessTestCase.configure(self, fixture, *args, **kwargs) self.num_clients = num_clients self.test_case_template.configure(fixture, *args, **kwargs) self.test_case_template.configure_shell() def _make_process(self): # This function should only be called by interface.py's as_command(). return self.test_case_template._make_process() def _get_shell_options_for_thread(self, thread_id): """ Get shell_options with an initialized TestData object for given thread. """ # We give each _SingleJSTestCase its own copy of the shell_options. shell_options = self.test_case_template.shell_options.copy() global_vars = shell_options["global_vars"].copy() test_data = global_vars["TestData"].copy() # We set a property on TestData to mark the main test when multiple clients are going to run # concurrently in case there is logic within the test that must execute only once. We also # set a property on TestData to indicate how many clients are going to run the test so they # can avoid executing certain logic when there may be other operations running concurrently. is_main_test = thread_id == 0 test_data["isMainTest"] = is_main_test test_data["numTestClients"] = self.num_clients global_vars["TestData"] = test_data shell_options["global_vars"] = global_vars return shell_options def _create_test_case_for_thread(self, logger, thread_id): """ Create and configure a _SingleJSTestCase to be run in a separate thread. """ shell_options = self._get_shell_options_for_thread(thread_id) test_case = _SingleJSTestCase(logger, self.test_case_template.js_filename, self.test_case_template.shell_executable, shell_options) test_case.configure(self.fixture) return test_case def _run_single_copy(self): test_case = self._create_test_case_for_thread(self.logger, thread_id=0) try: test_case.run_test() # If there was an exception, it will be logged in test_case's run_test function. finally: self.return_code = test_case.return_code def _run_multiple_copies(self): threads = [] test_cases = [] try: # If there are multiple clients, make a new thread for each client. for thread_id in xrange(self.num_clients): logger = self.logger.new_test_thread_logger(self.test_kind, str(thread_id)) test_case = self._create_test_case_for_thread(logger, thread_id) test_cases.append(test_case) thread = self.ThreadWithException(target=test_case.run_test) threads.append(thread) thread.start() except: self.logger.exception("Encountered an error starting threads for jstest %s.", self.basename()) raise finally: for thread in threads: thread.join() # Go through each test's return code and store the first nonzero one if it exists. return_code = 0 for test_case in test_cases: if test_case.return_code != 0: return_code = test_case.return_code break self.return_code = return_code for (thread_id, thread) in enumerate(threads): if thread.exc_info is not None: if not isinstance(thread.exc_info[1], self.failureException): self.logger.error( "Encountered an error inside thread %d running jstest %s.", thread_id, self.basename(), exc_info=thread.exc_info) raise thread.exc_info def run_test(self): if self.num_clients == 1: self._run_single_copy() else: self._run_multiple_copies()