Skip to content

Commit

Permalink
SERVER-31470 Move "run tests" logic into evergreen_run_tests.py.
Browse files Browse the repository at this point in the history
  • Loading branch information
visemet committed Oct 18, 2017
1 parent fb3b2eb commit 046a5a0
Show file tree
Hide file tree
Showing 13 changed files with 534 additions and 240 deletions.
152 changes: 152 additions & 0 deletions buildscripts/evergreen_run_tests.py
@@ -0,0 +1,152 @@
#!/usr/bin/env python

"""
Command line utility for executing MongoDB tests in Evergreen.
"""

from __future__ import absolute_import

import collections
import os.path
import sys

# Get relative imports to work when the package is not installed on the PYTHONPATH.
if __name__ == "__main__" and __package__ is None:
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from buildscripts import resmoke
from buildscripts import resmokelib


_TagInfo = collections.namedtuple("_TagInfo", ["tag_name", "evergreen_aware", "suite_options"])


class Main(resmoke.Main):
"""
A class for executing potentially multiple resmoke.py test suites in a way that handles
additional options for running unreliable tests in Evergreen.
"""

UNRELIABLE_TAG = _TagInfo(tag_name="unreliable",
evergreen_aware=True,
suite_options=resmokelib.config.SuiteOptions.ALL_INHERITED._replace(
report_failure_status="silentfail"))

RESOURCE_INTENSIVE_TAG = _TagInfo(
tag_name="resource_intensive",
evergreen_aware=False,
suite_options=resmokelib.config.SuiteOptions.ALL_INHERITED._replace(num_jobs=1))

RETRY_ON_FAILURE_TAG = _TagInfo(
tag_name="retry_on_failure",
evergreen_aware=True,
suite_options=resmokelib.config.SuiteOptions.ALL_INHERITED._replace(
fail_fast=False,
num_repeats=2,
report_failure_status="silentfail"))

def _make_evergreen_aware_tags(self, tag_name):
"""
Returns a list of resmoke.py tags for task, variant, and distro combinations in Evergreen.
"""

tags_format = ["{tag_name}"]

if resmokelib.config.EVERGREEN_TASK_NAME is not None:
tags_format.append("{tag_name}|{task_name}")

if resmokelib.config.EVERGREEN_VARIANT_NAME is not None:
tags_format.append("{tag_name}|{task_name}|{variant_name}")

if resmokelib.config.EVERGREEN_DISTRO_ID is not None:
tags_format.append("{tag_name}|{task_name}|{variant_name}|{distro_id}")

return [tag.format(tag_name=tag_name,
task_name=resmokelib.config.EVERGREEN_TASK_NAME,
variant_name=resmokelib.config.EVERGREEN_VARIANT_NAME,
distro_id=resmokelib.config.EVERGREEN_DISTRO_ID)
for tag in tags_format]

@classmethod
def _make_tag_combinations(cls):
"""
Returns a list of (tag, enabled) pairs representing all possible combinations of all
possible pairings of whether the tags are enabled or disabled together.
"""

combinations = []

if resmokelib.config.EVERGREEN_PATCH_BUILD:
combinations.append((
"unreliable and resource intensive",
((cls.UNRELIABLE_TAG, True), (cls.RESOURCE_INTENSIVE_TAG, True))))
combinations.append((
"unreliable and not resource intensive",
((cls.UNRELIABLE_TAG, True), (cls.RESOURCE_INTENSIVE_TAG, False))))
combinations.append((
"reliable and resource intensive",
((cls.UNRELIABLE_TAG, False), (cls.RESOURCE_INTENSIVE_TAG, True))))
combinations.append((
"reliable and not resource intensive",
((cls.UNRELIABLE_TAG, False), (cls.RESOURCE_INTENSIVE_TAG, False))))
else:
combinations.append((
"retry on failure and resource intensive",
((cls.RETRY_ON_FAILURE_TAG, True), (cls.RESOURCE_INTENSIVE_TAG, True))))
combinations.append((
"retry on failure and not resource intensive",
((cls.RETRY_ON_FAILURE_TAG, True), (cls.RESOURCE_INTENSIVE_TAG, False))))
combinations.append((
"run once and resource intensive",
((cls.RETRY_ON_FAILURE_TAG, False), (cls.RESOURCE_INTENSIVE_TAG, True))))
combinations.append((
"run once and not resource intensive",
((cls.RETRY_ON_FAILURE_TAG, False), (cls.RESOURCE_INTENSIVE_TAG, False))))

return combinations

def _get_suites(self):
"""
Returns a list of resmokelib.testing.suite.Suite instances to execute.
For every resmokelib.testing.suite.Suite instance returned by resmoke.Main._get_suites(),
multiple copies of that test suite are run using different resmokelib.config.SuiteOptions()
depending on whether each tag in the combination is enabled or not.
"""

suites = []

for suite in resmoke.Main._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.
suites.append(suite)
continue

for (tag_desc, tag_combo) in self._make_tag_combinations():
suite_options_list = []

for (tag_info, enabled) in tag_combo:
if tag_info.evergreen_aware:
tags = self._make_evergreen_aware_tags(tag_info.tag_name)
include_tags = {"$anyOf": tags}
else:
include_tags = tag_info.tag_name

if enabled:
suite_options = tag_info.suite_options._replace(include_tags=include_tags)
else:
suite_options = resmokelib.config.SuiteOptions.ALL_INHERITED._replace(
include_tags={"$not": include_tags})

suite_options_list.append(suite_options)

suite_options = resmokelib.config.SuiteOptions.combine(*suite_options_list)
suite_options = suite_options._replace(description=tag_desc)
suites.append(suite.with_options(suite_options))

return suites


if __name__ == "__main__":
Main().run()
6 changes: 3 additions & 3 deletions buildscripts/promote_silent_failures.py
Expand Up @@ -49,15 +49,15 @@ def main():
# Count number of "silentfail" per test file.
status_dict = collections.defaultdict(int)
for test_info in test_report.test_infos:
if test_info.status == "silentfail":
if test_info.evergreen_status == "silentfail":
status_dict[test_info.test_id] += 1

# For test files with more than 1 "silentfail", convert status to "fail".
for test_info in test_report.test_infos:
if status_dict[test_info.test_id] >= 2:
test_info.status = "fail"
test_info.evergreen_status = "fail"

result_report = test_report.as_dict();
result_report = test_report.as_dict()
if options.outfile != "-":
with open(options.outfile, "w") as fp:
json.dump(result_report, fp)
Expand Down
153 changes: 91 additions & 62 deletions buildscripts/resmoke.py
Expand Up @@ -30,14 +30,14 @@ def _execute_suite(suite):

if resmokelib.config.SHUFFLE:
logger.info("Shuffling order of tests for %ss in suite %s. The seed is %d.",
suite.test_kind, suite.get_name(), resmokelib.config.RANDOM_SEED)
suite.test_kind, suite.get_display_name(), resmokelib.config.RANDOM_SEED)
random.seed(resmokelib.config.RANDOM_SEED)
random.shuffle(suite.tests)

if resmokelib.config.DRY_RUN == "tests":
sb = []
sb.append("Tests that would be run for %ss in suite %s:"
% (suite.test_kind, suite.get_name()))
% (suite.test_kind, suite.get_display_name()))
if len(suite.tests) > 0:
for test in suite.tests:
sb.append(test)
Expand All @@ -48,25 +48,29 @@ def _execute_suite(suite):
# 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 True
return False

if len(suite.tests) == 0:
logger.info("Skipping %ss, no tests to run", suite.test_kind)
return True

# 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

executor_config = suite.get_executor_config()
executor = resmokelib.testing.executor.TestSuiteExecutor(logger, suite, **executor_config)

try:
executor.run()
if resmokelib.config.FAIL_FAST and suite.return_code != 0:
if suite.options.fail_fast and suite.return_code != 0:
return False
except resmokelib.errors.UserInterrupt:
suite.return_code = 130 # Simulate SIGINT as exit code.
return True
except:
logger.exception("Encountered an error when running %ss of suite %s.",
suite.test_kind, suite.get_name())
suite.test_kind, suite.get_display_name())
suite.return_code = 2
return False

Expand All @@ -90,7 +94,7 @@ def _dump_suite_config(suite, logging_config):
"""

sb = []
sb.append("YAML configuration of suite %s" % (suite.get_name()))
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()}))
Expand Down Expand Up @@ -120,75 +124,100 @@ def _list_suites_and_exit(logger, exit_code=0):
logger.info("Suites available to execute:\n%s", "\n".join(suite_names))
sys.exit(exit_code)

def main():
start_time = time.time()

values, args = resmokelib.parser.parse_command_line()
class Main(object):
"""
A class for executing potentially multiple resmoke.py test suites.
"""

def __init__(self):
"""
Initializes the Main instance by parsing the command line arguments.
"""

self.__start_time = time.time()

logging_config = resmokelib.parser.get_logging_config(values)
resmokelib.logging.loggers.configure_loggers(logging_config)
resmokelib.logging.flush.start_thread()
values, args = resmokelib.parser.parse_command_line()
self.__values = values
self.__args = args

resmokelib.parser.update_config_vars(values)
def _get_suites(self):
"""
Returns a list of resmokelib.testing.suite.Suite instances to execute.
"""

exec_logger = resmokelib.logging.loggers.EXECUTOR_LOGGER
resmoke_logger = exec_logger.new_resmoke_logger()
return resmokelib.parser.get_suites(self.__values, self.__args)

if values.list_suites:
_list_suites_and_exit(resmoke_logger)
def run(self):
"""
Executes the list of resmokelib.testing.suite.Suite instances returned by _get_suites().
"""

# 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))
logging_config = resmokelib.parser.get_logging_config(self.__values)
resmokelib.logging.loggers.configure_loggers(logging_config)
resmokelib.logging.flush.start_thread()

interrupted = False
try:
suites = resmokelib.parser.get_suites(values, args)
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, start_time)

# Run the suite finder after the test suite parsing is complete.
if 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)
resmokelib.parser.update_config_vars(self.__values)

try:
for suite in suites:
resmoke_logger.info(_dump_suite_config(suite, logging_config))
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)

try:
for suite in suites:
resmoke_logger.info(_dump_suite_config(suite, logging_config))

suite.record_suite_start()
interrupted = _execute_suite(suite)
suite.record_suite_end()
suite.record_suite_start()
interrupted = _execute_suite(suite)
suite.record_suite_end()

resmoke_logger.info("=" * 80)
resmoke_logger.info("Summary of %s suite: %s",
suite.get_name(), _summarize_suite(suite))
resmoke_logger.info("=" * 80)
resmoke_logger.info("Summary of %s suite: %s",
suite.get_display_name(), _summarize_suite(suite))

if interrupted or (resmokelib.config.FAIL_FAST and suite.return_code != 0):
time_taken = time.time() - start_time
_log_summary(resmoke_logger, suites, time_taken)
sys.exit(suite.return_code)
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)

time_taken = time.time() - start_time
_log_summary(resmoke_logger, suites, time_taken)
time_taken = time.time() - self.__start_time
_log_summary(resmoke_logger, suites, time_taken)

# 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()
# 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)
resmokelib.reportfile.write(suites)


if __name__ == "__main__":
main()
Main().run()

0 comments on commit 046a5a0

Please sign in to comment.