summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbuildscripts/evergreen_run_tests.py8
-rwxr-xr-xbuildscripts/resmoke.py400
-rw-r--r--buildscripts/resmokelib/parser.py30
3 files changed, 230 insertions, 208 deletions
diff --git a/buildscripts/evergreen_run_tests.py b/buildscripts/evergreen_run_tests.py
index 69520dd472e..cec88b86d0f 100755
--- a/buildscripts/evergreen_run_tests.py
+++ b/buildscripts/evergreen_run_tests.py
@@ -17,7 +17,7 @@ from buildscripts import resmokelib # pylint: disable=wrong-import-position
_TagInfo = collections.namedtuple("_TagInfo", ["tag_name", "evergreen_aware", "suite_options"])
-class Main(resmoke.Main):
+class Main(resmoke.Resmoke):
"""Execute Main class.
A class for executing potentially multiple resmoke.py test suites in a way that handles
@@ -112,7 +112,7 @@ class Main(resmoke.Main):
suites = []
- for suite in resmoke.Main._get_suites(self):
+ for suite in resmoke.Resmoke._get_suites(self):
if suite.test_kind != "js_test":
# Tags are only support for JavaScript tests, so we leave the test suite alone when
# running any other kind of test.
@@ -145,4 +145,6 @@ class Main(resmoke.Main):
if __name__ == "__main__":
- Main().run()
+ main = Main()
+ main.configure_from_command_line()
+ main.run()
diff --git a/buildscripts/resmoke.py b/buildscripts/resmoke.py
index 033fab1889d..0d57e3ff6f2 100755
--- a/buildscripts/resmoke.py
+++ b/buildscripts/resmoke.py
@@ -12,222 +12,234 @@ import time
if __name__ == "__main__" and __package__ is None:
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from buildscripts import resmokelib # pylint: disable=wrong-import-position
+# pylint: disable=wrong-import-position
+from buildscripts.resmokelib import config
+from buildscripts.resmokelib import errors
+from buildscripts.resmokelib import logging
+from buildscripts.resmokelib import parser
+from buildscripts.resmokelib import reportfile
+from buildscripts.resmokelib import sighandler
+from buildscripts.resmokelib import suitesconfig
+from buildscripts.resmokelib import testing
+from buildscripts.resmokelib import utils
-def _execute_suite(suite): # pylint: disable=too-many-branches,too-many-return-statements
- """Execute the test suite, failing fast if requested.
+class Resmoke(object):
+ """The main class to run tests with resmoke."""
- Return true if the execution of the suite was interrupted by the
- user, and false otherwise.
- """
-
- logger = resmokelib.logging.loggers.EXECUTOR_LOGGER
+ def __init__(self):
+ """Initialize the Resmoke instance."""
+ self.__start_time = time.time()
+ self._config = None
+ self._exec_logger = None
+ self._resmoke_logger = None
+ self._archive = None
+ self._interrupted = False
+
+ def configure_from_command_line(self):
+ """Configure this instance using the command line arguments."""
+ self._config = parser.parse_command_line()
+
+ def _setup_logging(self):
+ logging.loggers.configure_loggers(self._config.logging_config)
+ logging.flush.start_thread()
+ self._exec_logger = logging.loggers.EXECUTOR_LOGGER
+ self._resmoke_logger = self._exec_logger.new_resmoke_logger()
- if resmokelib.config.SHUFFLE:
- logger.info("Shuffling order of tests for %ss in suite %s. The seed is %d.",
- suite.test_kind, suite.get_display_name(), resmokelib.config.RANDOM_SEED)
- random.seed(resmokelib.config.RANDOM_SEED)
- random.shuffle(suite.tests)
+ def run(self):
+ """Run resmoke."""
+ if self._config is None:
+ raise RuntimeError("Resmoke must be configured before calling run()")
+ self._setup_logging()
- if resmokelib.config.DRY_RUN == "tests":
- sb = []
- sb.append("Tests that would be run in suite %s:" % suite.get_display_name())
- if suite.tests:
+ try:
+ if self._config.list_suites:
+ self.list_suites()
+ elif self._config.find_suites:
+ self.find_suites()
+ elif self._config.dry_run == "tests":
+ self.dry_run()
+ else:
+ self.run_tests()
+ finally:
+ if not self._interrupted:
+ logging.flush.stop_thread()
+
+ def list_suites(self):
+ """List the suites that are available to execute."""
+ suite_names = suitesconfig.get_named_suites()
+ self._resmoke_logger.info("Suites available to execute:\n%s", "\n".join(suite_names))
+
+ def find_suites(self):
+ """List the suites that run the specified tests."""
+ suites = self._get_suites()
+ suites_by_test = self._find_suites_by_test(suites)
+ for test in sorted(suites_by_test):
+ suite_names = suites_by_test[test]
+ self._resmoke_logger.info("%s will be run by the following suite(s): %s", test,
+ suite_names)
+
+ @staticmethod
+ def _find_suites_by_test(suites):
+ """
+ Look up what other resmoke suites run the tests specified in the suites parameter.
+
+ Return a dict keyed by test name, value is array of suite names.
+ """
+ memberships = {}
+ test_membership = suitesconfig.create_test_membership_map()
+ for suite in suites:
for test in suite.tests:
- sb.append(test)
- else:
- sb.append("(no tests)")
- logger.info("\n".join(sb))
- sb = []
- sb.append("Tests that would be excluded from suite %s:" % suite.get_display_name())
- if suite.excluded:
- for test in suite.excluded:
- sb.append(test)
- else:
- sb.append("(no tests)")
- logger.info("\n".join(sb))
-
- # Set a successful return code on the test suite because we want to output the tests
- # that would get run by any other suites the user specified.
- suite.return_code = 0
- return False
-
- if not suite.tests:
- logger.info("Skipping %ss, no tests to run", suite.test_kind)
+ memberships[test] = test_membership[test]
+ return memberships
+
+ def dry_run(self):
+ """List which tests would run and which tests would be excluded in a resmoke invocation."""
+ suites = self._get_suites()
+ for suite in suites:
+ self._shuffle_tests(suite)
+ sb = ["Tests that would be run in suite {}".format(suite.get_display_name())]
+ sb.extend(suite.tests or ["(no tests)"])
+ sb.append("Tests that would be excluded from suite {}".format(suite.get_display_name()))
+ sb.extend(suite.excluded or ["(no tests)"])
+ self._exec_logger.info("\n".join(sb))
+
+ def run_tests(self):
+ """Run the suite and tests specified."""
+ self._resmoke_logger.info("resmoke.py invocation: %s", " ".join(sys.argv))
+ suites = None
+ try:
+ suites = self._get_suites()
- # Set a successful return code on the test suite because we want to output the tests
- # that would get run by any other suites the user specified.
- suite.return_code = 0
- return False
+ self._setup_archival()
+ self._setup_signal_handler(suites)
- archive = None
- if resmokelib.config.ARCHIVE_FILE:
- archive = resmokelib.utils.archival.Archival(
- archival_json_file=resmokelib.config.ARCHIVE_FILE,
- limit_size_mb=resmokelib.config.ARCHIVE_LIMIT_MB,
- limit_files=resmokelib.config.ARCHIVE_LIMIT_TESTS, logger=logger)
+ for suite in suites:
+ self._interrupted = self._run_suite(suite)
+ if self._interrupted or (suite.options.fail_fast and suite.return_code != 0):
+ self._log_resmoke_summary(suites)
+ self.exit(suite.return_code)
- executor_config = suite.get_executor_config()
- executor = resmokelib.testing.executor.TestSuiteExecutor(
- logger, suite, archive_instance=archive, **executor_config)
+ self._log_resmoke_summary(suites)
- try:
- executor.run()
- if suite.options.fail_fast and suite.return_code != 0:
+ # Exit with a nonzero code if any of the suites failed.
+ exit_code = max(suite.return_code for suite in suites)
+ self.exit(exit_code)
+ finally:
+ if not self._interrupted:
+ self._exit_archival()
+ if suites:
+ reportfile.write(suites)
+
+ def _run_suite(self, suite):
+ """Run a test suite."""
+ self._log_suite_config(suite)
+ suite.record_suite_start()
+ interrupted = self._execute_suite(suite)
+ suite.record_suite_end()
+ self._log_suite_summary(suite)
+ return interrupted
+
+ def _log_resmoke_summary(self, suites):
+ """Log a summary of the resmoke run."""
+ time_taken = time.time() - self.__start_time
+ if len(self._config.suite_files) > 1:
+ testing.suite.Suite.log_summaries(self._resmoke_logger, suites, time_taken)
+
+ def _log_suite_summary(self, suite):
+ """Log a summary of the suite run."""
+ self._resmoke_logger.info("=" * 80)
+ self._resmoke_logger.info("Summary of %s suite: %s", suite.get_display_name(),
+ self._get_suite_summary(suite))
+
+ def _execute_suite(self, suite):
+ """Execute a suite and return True if interrupted, False otherwise. """
+ self._shuffle_tests(suite)
+ if not suite.tests:
+ self._exec_logger.info("Skipping %s, no tests to run", suite.test_kind)
+ suite.return_code = 0
+ return False
+ executor_config = suite.get_executor_config()
+ executor = testing.executor.TestSuiteExecutor(
+ self._exec_logger, suite, archive_instance=self._archive, **executor_config)
+ try:
+ executor.run()
+ except errors.UserInterrupt:
+ suite.return_code = 130 # Simulate SIGINT as exit code.
+ return True
+ except IOError:
+ suite.return_code = 74 # Exit code for IOError on POSIX systems.
+ return True
+ except: # pylint: disable=bare-except
+ self._exec_logger.exception("Encountered an error when running %ss of suite %s.",
+ suite.test_kind, suite.get_display_name())
+ suite.return_code = 2
return False
- except resmokelib.errors.UserInterrupt:
- suite.return_code = 130 # Simulate SIGINT as exit code.
- return True
- except IOError:
- suite.return_code = 74 # Exit code for IOError on POSIX systems.
- return True
- except: # pylint: disable=bare-except
- logger.exception("Encountered an error when running %ss of suite %s.", suite.test_kind,
- suite.get_display_name())
- suite.return_code = 2
return False
- finally:
- if archive:
- archive.exit()
- return False
-
-
-def _log_summary(logger, suites, time_taken):
- if len(suites) > 1:
- resmokelib.testing.suite.Suite.log_summaries(logger, suites, time_taken)
-
-
-def _summarize_suite(suite):
- sb = []
- suite.summarize(sb)
- return "\n".join(sb)
-
-
-def _dump_suite_config(suite, logging_config):
- """Return a string that represents the YAML configuration of a suite.
-
- TODO: include the "options" key in the result
- """
-
- sb = []
- sb.append("YAML configuration of suite %s" % (suite.get_display_name()))
- sb.append(resmokelib.utils.dump_yaml({"test_kind": suite.get_test_kind_config()}))
- sb.append("")
- sb.append(resmokelib.utils.dump_yaml({"selector": suite.get_selector_config()}))
- sb.append("")
- sb.append(resmokelib.utils.dump_yaml({"executor": suite.get_executor_config()}))
- sb.append("")
- sb.append(resmokelib.utils.dump_yaml({"logging": logging_config}))
- return "\n".join(sb)
-
-def find_suites_by_test(suites):
- """Look up what other resmoke suites run the tests specified in the suites parameter.
-
- Return a dict keyed by test name, value is array of suite names.
- """
-
- memberships = {}
- test_membership = resmokelib.suitesconfig.create_test_membership_map()
- for suite in suites:
- for test in suite.tests:
- memberships[test] = test_membership[test]
- return memberships
-
-
-def _list_suites_and_exit(logger, exit_code=0):
- suite_names = resmokelib.suitesconfig.get_named_suites()
- logger.info("Suites available to execute:\n%s", "\n".join(suite_names))
- sys.exit(exit_code)
-
-
-class Main(object):
- """A class for executing potentially multiple resmoke.py test suites."""
-
- def __init__(self):
- """Initialize the Main instance by parsing the command line arguments."""
-
- self.__start_time = time.time()
-
- values, args = resmokelib.parser.parse_command_line()
- self.__values = values
- self.__args = args
+ def _shuffle_tests(self, suite):
+ """Shuffle the tests if the shuffle cli option was set."""
+ random.seed(config.RANDOM_SEED)
+ if not config.SHUFFLE:
+ return
+ self._exec_logger.info("Shuffling order of tests for %ss in suite %s. The seed is %d.",
+ suite.test_kind, suite.get_display_name(), config.RANDOM_SEED)
+ random.shuffle(suite.tests)
def _get_suites(self):
- """Return a list of resmokelib.testing.suite.Suite instances to execute."""
-
- return resmokelib.suitesconfig.get_suites(
- suite_files=self.__values.suite_files.split(","), test_files=self.__args)
-
- def run(self):
- """Execute the list of resmokelib.testing.suite.Suite instances."""
-
- logging_config = resmokelib.parser.get_logging_config(self.__values)
- resmokelib.logging.loggers.configure_loggers(logging_config)
- resmokelib.logging.flush.start_thread()
-
- resmokelib.parser.update_config_vars(self.__values)
-
- exec_logger = resmokelib.logging.loggers.EXECUTOR_LOGGER
- resmoke_logger = exec_logger.new_resmoke_logger()
-
- if self.__values.list_suites:
- _list_suites_and_exit(resmoke_logger)
-
- # Log the command line arguments specified to resmoke.py to make it easier to re-run the
- # resmoke.py invocation used by an Evergreen task.
- resmoke_logger.info("resmoke.py invocation: %s", " ".join(sys.argv))
-
- interrupted = False
- try:
- suites = self._get_suites()
- except resmokelib.errors.SuiteNotFound as err:
- resmoke_logger.error("Failed to parse YAML suite definition: %s", str(err))
- _list_suites_and_exit(resmoke_logger, exit_code=1)
-
- # Register a signal handler or Windows event object so we can write the report file if the
- # task times out.
- resmokelib.sighandler.register(resmoke_logger, suites, self.__start_time)
-
- # Run the suite finder after the test suite parsing is complete.
- if self.__values.find_suites:
- suites_by_test = find_suites_by_test(suites)
- for test in sorted(suites_by_test):
- suite_names = suites_by_test[test]
- resmoke_logger.info("%s will be run by the following suite(s): %s", test,
- suite_names)
- sys.exit(0)
-
+ """Return the list of suites for this resmoke invocation."""
try:
- for suite in suites:
- resmoke_logger.info(_dump_suite_config(suite, logging_config))
+ return suitesconfig.get_suites(self._config.suite_files, self._config.test_files)
+ except errors.SuiteNotFound as err:
+ self._resmoke_logger.error("Failed to parse YAML suite definition: %s", str(err))
+ self.list_suites()
+ self.exit(1)
+
+ def _log_suite_config(self, suite):
+ sb = [
+ "YAML configuration of suite {}".format(suite.get_display_name()),
+ utils.dump_yaml({"test_kind": suite.get_test_kind_config()}), "",
+ utils.dump_yaml({"selector": suite.get_selector_config()}), "",
+ utils.dump_yaml({"executor": suite.get_executor_config()}), "",
+ utils.dump_yaml({"logging": self._config.logging_config})
+ ]
+ self._resmoke_logger.info("\n".join(sb))
+
+ @staticmethod
+ def _get_suite_summary(suite):
+ """Return a summary of the suite run."""
+ sb = []
+ suite.summarize(sb)
+ return "\n".join(sb)
- suite.record_suite_start()
- interrupted = _execute_suite(suite)
- suite.record_suite_end()
+ def _setup_signal_handler(self, suites):
+ """Set up a SIGUSR1 signal handler that logs a test result summary and a thread dump."""
+ sighandler.register(self._resmoke_logger, suites, self.__start_time)
- resmoke_logger.info("=" * 80)
- resmoke_logger.info("Summary of %s suite: %s", suite.get_display_name(),
- _summarize_suite(suite))
+ def _setup_archival(self):
+ """Set up the archival feature if enabled in the cli options."""
+ if config.ARCHIVE_FILE:
+ self._archive = utils.archival.Archival(
+ archival_json_file=config.ARCHIVE_FILE, limit_size_mb=config.ARCHIVE_LIMIT_MB,
+ limit_files=config.ARCHIVE_LIMIT_TESTS, logger=self._exec_logger)
- if interrupted or (suite.options.fail_fast and suite.return_code != 0):
- time_taken = time.time() - self.__start_time
- _log_summary(resmoke_logger, suites, time_taken)
- sys.exit(suite.return_code)
+ def _exit_archival(self):
+ """Finish up archival tasks before exit if enabled in the cli options."""
+ if self._archive:
+ self._archive.exit()
- time_taken = time.time() - self.__start_time
- _log_summary(resmoke_logger, suites, time_taken)
+ def exit(self, exit_code):
+ """Exit with the provided exit code."""
+ self._resmoke_logger.info("Exiting with code: %d", exit_code)
+ sys.exit(exit_code)
- # Exit with a nonzero code if any of the suites failed.
- exit_code = max(suite.return_code for suite in suites)
- sys.exit(exit_code)
- finally:
- if not interrupted:
- resmokelib.logging.flush.stop_thread()
- resmokelib.reportfile.write(suites)
+def main():
+ """Main function for resmoke."""
+ resmoke = Resmoke()
+ resmoke.configure_from_command_line()
+ resmoke.run()
if __name__ == "__main__":
- Main().run()
+ main()
diff --git a/buildscripts/resmokelib/parser.py b/buildscripts/resmokelib/parser.py
index d9f40da3e90..9dc464d8129 100644
--- a/buildscripts/resmokelib/parser.py
+++ b/buildscripts/resmokelib/parser.py
@@ -2,6 +2,7 @@
from __future__ import absolute_import
+import collections
import os
import os.path
@@ -12,10 +13,13 @@ from . import config as _config
from . import utils
from .. import resmokeconfig
+ResmokeConfig = collections.namedtuple(
+ "ResmokeConfig",
+ ["list_suites", "find_suites", "dry_run", "suite_files", "test_files", "logging_config"])
-def parse_command_line(): # pylint: disable=too-many-statements
- """Parse the command line arguments passed to resmoke.py."""
+def _make_parser(): # pylint: disable=too-many-statements
+ """Create and return the command line arguments parser."""
parser = optparse.OptionParser()
parser.add_option("--suites", dest="suite_files", metavar="SUITE1,SUITE2",
@@ -291,15 +295,23 @@ def parse_command_line(): # pylint: disable=too-many-statements
parser.set_defaults(logger_file="console", dry_run="off", find_suites=False, list_suites=False,
suite_files="with_server", prealloc_journal="off", shuffle="auto",
stagger_jobs="off")
+ return parser
+
+def parse_command_line():
+ """Parses the command line arguments passed to resmoke.py."""
+ parser = _make_parser()
options, args = parser.parse_args()
- validate_options(parser, options, args)
+ _validate_options(parser, options, args)
+ _update_config_vars(options)
- return options, args
+ return ResmokeConfig(list_suites=options.list_suites, find_suites=options.find_suites,
+ dry_run=options.dry_run, suite_files=options.suite_files.split(","),
+ test_files=args, logging_config=_get_logging_config(options.logger_file))
-def validate_options(parser, options, args):
+def _validate_options(parser, options, args):
"""Do preliminary validation on the options and error on any invalid options."""
if options.shell_port is not None and options.shell_conn_string is not None:
@@ -329,13 +341,9 @@ def validate_benchmark_options():
"results. Please use --jobs=1" % _config.JOBS)
-def get_logging_config(values):
- """Return logging config values."""
- return _get_logging_config(values.logger_file)
-
+def _update_config_vars(values): # pylint: disable=too-many-statements
+ """Update the variables of the config module."""
-def update_config_vars(values): # pylint: disable=too-many-statements
- """Update config vars."""
config = _config.DEFAULTS.copy()
# Override `config` with values from command line arguments.