mirror of https://github.com/mongodb/mongo
939 lines
37 KiB
Python
939 lines
37 KiB
Python
"""Test resmoke's handling of test/task timeouts and archival."""
|
|
|
|
import datetime
|
|
import io
|
|
import json
|
|
import logging
|
|
import os
|
|
import os.path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import unittest
|
|
from shutil import rmtree
|
|
from typing import List
|
|
|
|
import yaml
|
|
|
|
from buildscripts.ciconfig.evergreen import parse_evergreen_file
|
|
from buildscripts.idl.gen_all_feature_flag_list import get_all_feature_flags_turned_off_by_default
|
|
from buildscripts.resmokelib import config, core, suitesconfig
|
|
from buildscripts.resmokelib.hang_analyzer.attach_core_analyzer_task import (
|
|
matches_generated_task_pattern,
|
|
)
|
|
from buildscripts.resmokelib.hang_analyzer.gen_hang_analyzer_tasks import get_generated_task_name
|
|
from buildscripts.resmokelib.utils.dictionary import get_dict_value
|
|
|
|
|
|
class _ResmokeSelftest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.end2end_root = "buildscripts/tests/resmoke_end2end"
|
|
cls.test_dir = os.path.normpath("/data/db/selftest")
|
|
cls.resmoke_const_args = ["run", "--dbpathPrefix={}".format(cls.test_dir)]
|
|
cls.suites_root = os.path.join(cls.end2end_root, "suites")
|
|
cls.testfiles_root = os.path.join(cls.end2end_root, "testfiles")
|
|
cls.report_file = os.path.join(cls.test_dir, "reports.json")
|
|
|
|
cls.resmoke_process = None
|
|
|
|
def setUp(self):
|
|
self.logger = logging.getLogger(self._testMethodName)
|
|
self.logger.setLevel(logging.DEBUG)
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
handler.setFormatter(logging.Formatter(fmt="%(message)s"))
|
|
self.logger.addHandler(handler)
|
|
|
|
self.logger.info("Cleaning temp directory %s", self.test_dir)
|
|
rmtree(self.test_dir, ignore_errors=True)
|
|
os.makedirs(self.test_dir, mode=0o755, exist_ok=True)
|
|
|
|
def execute_resmoke(self, resmoke_args, **kwargs):
|
|
resmoke_process = core.programs.make_process(
|
|
self.logger,
|
|
[sys.executable, "buildscripts/resmoke.py"] + self.resmoke_const_args + resmoke_args,
|
|
)
|
|
resmoke_process.start()
|
|
self.resmoke_process = resmoke_process
|
|
|
|
def assert_dir_file_count(self, test_dir, test_file, num_entries):
|
|
file_path = os.path.join(test_dir, test_file)
|
|
count = 0
|
|
with open(file_path) as file:
|
|
count = sum(1 for _ in file)
|
|
self.assertEqual(count, num_entries)
|
|
|
|
|
|
class TestArchivalOnFailure(_ResmokeSelftest):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestArchivalOnFailure, cls).setUpClass()
|
|
|
|
cls.archival_file = "test_archival.txt"
|
|
|
|
def test_archival_on_task_failure(self):
|
|
# The --originSuite argument is to trick the resmoke local invocation into passing
|
|
# because when we pass --taskId into resmoke it thinks that it is being ran in evergreen
|
|
# and cannot normally find an evergreen task associated with
|
|
# buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_failure.yml
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_failure.yml",
|
|
"--originSuite=resmoke_end2end_tests",
|
|
"--taskId=123",
|
|
"--internalParam=test_archival",
|
|
"--repeatTests=2",
|
|
"--jobs=2",
|
|
]
|
|
self.execute_resmoke(resmoke_args)
|
|
self.resmoke_process.wait()
|
|
|
|
# test archival
|
|
archival_dirs_to_expect = 4 # 2 tests * 2 nodes
|
|
self.assert_dir_file_count(self.test_dir, self.archival_file, archival_dirs_to_expect)
|
|
|
|
def test_archival_on_task_failure_no_passthrough(self):
|
|
# The --originSuite argument is to trick the resmoke local invocation into passing
|
|
# because when we pass --taskId into resmoke it thinks that it is being ran in evergreen
|
|
# and cannot normally find an evergreen task associated with
|
|
# buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_failure_no_passthrough.yml
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_failure_no_passthrough.yml",
|
|
"--taskId=123",
|
|
"--originSuite=resmoke_end2end_tests",
|
|
"--internalParam=test_archival",
|
|
"--repeatTests=2",
|
|
"--jobs=2",
|
|
]
|
|
self.execute_resmoke(resmoke_args)
|
|
self.resmoke_process.wait()
|
|
|
|
# test archival
|
|
archival_dirs_to_expect = 8 # (2 tests + 2 stacktrace files) * 2 nodes
|
|
self.assert_dir_file_count(self.test_dir, self.archival_file, archival_dirs_to_expect)
|
|
|
|
def test_no_archival_locally(self):
|
|
# archival should not happen if --taskId is not set.
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_failure_no_passthrough.yml",
|
|
"--internalParam=test_archival",
|
|
"--repeatTests=2",
|
|
"--jobs=2",
|
|
]
|
|
self.execute_resmoke(resmoke_args)
|
|
self.resmoke_process.wait()
|
|
|
|
# test that archival file wasn't created.
|
|
self.assertFalse(os.path.exists(self.archival_file))
|
|
|
|
|
|
class TestTimeout(_ResmokeSelftest):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestTimeout, cls).setUpClass()
|
|
|
|
cls.test_dir_inner = os.path.normpath("/data/db/selftest_inner")
|
|
cls.archival_file = "test_archival.txt"
|
|
cls.analysis_file = "test_analysis.txt"
|
|
|
|
def setUp(self):
|
|
super(TestTimeout, self).setUp()
|
|
self.logger.info("Cleaning temp directory %s", self.test_dir_inner)
|
|
rmtree(self.test_dir_inner, ignore_errors=True)
|
|
|
|
def signal_resmoke(self):
|
|
hang_analyzer_options = (
|
|
f"-o=file -o=stdout -m=contains -p=python -d={self.resmoke_process.pid}"
|
|
)
|
|
signal_resmoke_process = core.programs.make_process(
|
|
self.logger,
|
|
[sys.executable, "buildscripts/resmoke.py", "hang-analyzer"]
|
|
+ hang_analyzer_options.split(),
|
|
)
|
|
signal_resmoke_process.start()
|
|
|
|
# Wait for resmoke_process to be killed by 'run-timeout' so this doesn't hang.
|
|
self.resmoke_process.wait()
|
|
|
|
return_code = signal_resmoke_process.wait()
|
|
if return_code != 0:
|
|
self.resmoke_process.stop()
|
|
self.assertEqual(return_code, 0)
|
|
|
|
def execute_resmoke(self, resmoke_args, sentinel_file, **kwargs):
|
|
# Since this test is designed to start remoke, wait for it to be up-and-running, and then
|
|
# kill resmoke, we use a sentinel file to accomplish this.
|
|
|
|
# Form sentinel path, remove any leftover version, and create the file that will be removed by the jstest.
|
|
sentinel_path = f"{os.environ.get('TMPDIR') or os.environ.get('TMP_DIR') or '/tmp'}/{sentinel_file}.js.sentinel"
|
|
if os.path.isfile(sentinel_path):
|
|
os.remove(sentinel_path)
|
|
open(sentinel_path, "w").close()
|
|
|
|
# Spawn resmoke (async):
|
|
super(TestTimeout, self).execute_resmoke(resmoke_args, **kwargs)
|
|
|
|
# Wait for sentinel file to disappear; bail if it takes too long:
|
|
started_polling_datetime = datetime.datetime.now()
|
|
while os.path.isfile(sentinel_path):
|
|
time.sleep(0.1)
|
|
if datetime.datetime.now() - started_polling_datetime > datetime.timedelta(minutes=5):
|
|
self.fail("SUT is not available within 5 minutes; aborting test")
|
|
|
|
# Kill resmoke:
|
|
self.signal_resmoke()
|
|
|
|
def test_task_timeout(self):
|
|
# The --originSuite argument is to trick the resmoke local invocation into passing
|
|
# because when we pass --taskId into resmoke it thinks that it is being ran in evergreen
|
|
# and cannot normally find an evergreen task associated with
|
|
# buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_timeout.yml
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_timeout.yml",
|
|
"--taskId=123",
|
|
"--originSuite=resmoke_end2end_tests",
|
|
"--internalParam=test_archival",
|
|
"--internalParam=test_analysis",
|
|
"--repeatTests=2",
|
|
"--jobs=2",
|
|
]
|
|
self.execute_resmoke(resmoke_args, sentinel_file="timeout0")
|
|
|
|
archival_dirs_to_expect = 4 # 2 tests * 2 mongod
|
|
self.assert_dir_file_count(self.test_dir, self.archival_file, archival_dirs_to_expect)
|
|
|
|
analysis_pids_to_expect = 6 # 2 tests * (2 mongod + 1 mongo)
|
|
self.assert_dir_file_count(self.test_dir, self.analysis_file, analysis_pids_to_expect)
|
|
|
|
def test_task_timeout_no_passthrough(self):
|
|
# The --originSuite argument is to trick the resmoke local invocation into passing
|
|
# because when we pass --taskId into resmoke it thinks that it is being ran in evergreen
|
|
# and cannot normally find an evergreen task associated with
|
|
# buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_timeout_no_passthrough.yml
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_task_timeout_no_passthrough.yml",
|
|
"--taskId=123",
|
|
"--originSuite=resmoke_end2end_tests",
|
|
"--internalParam=test_archival",
|
|
"--internalParam=test_analysis",
|
|
"--repeatTests=2",
|
|
"--jobs=2",
|
|
]
|
|
self.execute_resmoke(resmoke_args, sentinel_file="timeout1")
|
|
|
|
archival_dirs_to_expect = 8 # (2 tests + 2 stacktrace files) * 2 nodes
|
|
self.assert_dir_file_count(self.test_dir, self.archival_file, archival_dirs_to_expect)
|
|
|
|
analysis_pids_to_expect = 6 # 2 tests * (2 mongod + 1 mongo)
|
|
self.assert_dir_file_count(self.test_dir, self.analysis_file, analysis_pids_to_expect)
|
|
|
|
# Test scenarios where an resmoke-launched process launches resmoke.
|
|
def test_nested_timeout(self):
|
|
# The --originSuite argument is to trick the resmoke local invocation into passing
|
|
# because when we pass --taskId into resmoke it thinks that it is being ran in evergreen
|
|
# and cannot normally find an evergreen task associated with
|
|
# buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_nested_timeout.yml
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_nested_timeout.yml",
|
|
"--taskId=123",
|
|
"--originSuite=resmoke_end2end_tests",
|
|
"--internalParam=test_archival",
|
|
"--internalParam=test_analysis",
|
|
"jstests/resmoke_selftest/end2end/timeout/nested/top_level_timeout.js",
|
|
]
|
|
|
|
self.execute_resmoke(resmoke_args, sentinel_file="inner_level_timeout")
|
|
|
|
archival_dirs_to_expect = (
|
|
4 # ((2 tests + 2 stacktrace files) * 2 nodes) / 2 data_file directories
|
|
)
|
|
self.assert_dir_file_count(self.test_dir, self.archival_file, archival_dirs_to_expect)
|
|
self.assert_dir_file_count(self.test_dir_inner, self.archival_file, archival_dirs_to_expect)
|
|
|
|
analysis_pids_to_expect = 6 # 2 tests * (2 mongod + 1 mongo)
|
|
self.assert_dir_file_count(self.test_dir, self.analysis_file, analysis_pids_to_expect)
|
|
|
|
|
|
class TestTestSelection(_ResmokeSelftest):
|
|
def parse_reports_json(self):
|
|
with open(self.report_file) as fd:
|
|
return json.load(fd)
|
|
|
|
def execute_resmoke(self, resmoke_args):
|
|
resmoke_process = core.programs.make_process(
|
|
self.logger, [sys.executable, "buildscripts/resmoke.py", "run"] + resmoke_args
|
|
)
|
|
resmoke_process.start()
|
|
|
|
return resmoke_process
|
|
|
|
def create_file_in_test_dir(self, filename, contents):
|
|
with open(os.path.normpath(f"{self.test_dir}/{filename}"), "w") as fd:
|
|
fd.write(contents)
|
|
|
|
def get_tests_run(self):
|
|
tests_run = []
|
|
for res in self.parse_reports_json()["results"]:
|
|
if "fixture" not in res["test_file"]:
|
|
tests_run.append(res["test_file"])
|
|
|
|
return tests_run
|
|
|
|
def test_missing_excluded_file(self):
|
|
# Tests a suite that excludes a missing file
|
|
self.assertEqual(
|
|
0,
|
|
self.execute_resmoke(
|
|
[
|
|
f"--reportFile={self.report_file}",
|
|
"--repeatTests=2",
|
|
f"--suites={self.suites_root}/resmoke_missing_test.yml",
|
|
f"{self.testfiles_root}/one.js",
|
|
f"{self.testfiles_root}/one.js",
|
|
f"{self.testfiles_root}/one.js",
|
|
]
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(6 * [f"{self.testfiles_root}/one.js"], self.get_tests_run())
|
|
|
|
def test_positional_arguments(self):
|
|
self.assertEqual(
|
|
0,
|
|
self.execute_resmoke(
|
|
[
|
|
f"--reportFile={self.report_file}",
|
|
"--repeatTests=2",
|
|
f"--suites={self.suites_root}/resmoke_no_mongod.yml",
|
|
f"{self.testfiles_root}/one.js",
|
|
f"{self.testfiles_root}/one.js",
|
|
f"{self.testfiles_root}/one.js",
|
|
]
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(6 * [f"{self.testfiles_root}/one.js"], self.get_tests_run())
|
|
|
|
def test_replay_file(self):
|
|
self.create_file_in_test_dir("replay", f"{self.testfiles_root}/two.js\n" * 3)
|
|
|
|
self.assertEqual(
|
|
0,
|
|
self.execute_resmoke(
|
|
[
|
|
f"--reportFile={self.report_file}",
|
|
"--repeatTests=2",
|
|
f"--suites={self.suites_root}/resmoke_no_mongod.yml",
|
|
f"--replay={self.test_dir}/replay",
|
|
]
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(6 * [f"{self.testfiles_root}/two.js"], self.get_tests_run())
|
|
|
|
def test_suite_file(self):
|
|
self.assertEqual(
|
|
0,
|
|
self.execute_resmoke(
|
|
[
|
|
f"--reportFile={self.report_file}",
|
|
"--repeatTests=2",
|
|
f"--suites={self.suites_root}/resmoke_no_mongod.yml",
|
|
]
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(
|
|
2 * [f"{self.testfiles_root}/one.js", f"{self.testfiles_root}/two.js"],
|
|
self.get_tests_run(),
|
|
)
|
|
|
|
def test_at_sign_as_replay_file(self):
|
|
self.create_file_in_test_dir("replay", f"{self.testfiles_root}/two.js\n" * 3)
|
|
|
|
self.assertEqual(
|
|
0,
|
|
self.execute_resmoke(
|
|
[
|
|
f"--reportFile={self.report_file}",
|
|
"--repeatTests=2",
|
|
f"--suites={self.suites_root}/resmoke_no_mongod.yml",
|
|
f"@{self.test_dir}/replay",
|
|
]
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(6 * [f"{self.testfiles_root}/two.js"], self.get_tests_run())
|
|
|
|
def test_disallow_mixing_replay_and_positional(self):
|
|
self.create_file_in_test_dir("replay", f"{self.testfiles_root}/two.js\n" * 3)
|
|
|
|
# Additionally can assert on the error message.
|
|
self.assertEqual(
|
|
2,
|
|
self.execute_resmoke(
|
|
[f"--replay={self.test_dir}/replay", f"{self.testfiles_root}/one.js"]
|
|
).wait(),
|
|
)
|
|
|
|
# When multiple positional arguments are presented, they're all treated as test files.
|
|
self.assertEqual(
|
|
2,
|
|
self.execute_resmoke(
|
|
[f"@{self.test_dir}/replay", f"{self.testfiles_root}/one.js"]
|
|
).wait(),
|
|
)
|
|
self.assertEqual(
|
|
2,
|
|
self.execute_resmoke(
|
|
[f"{self.testfiles_root}/one.js", f"@{self.test_dir}/replay"]
|
|
).wait(),
|
|
)
|
|
|
|
|
|
class TestSetParameters(_ResmokeSelftest):
|
|
def setUp(self):
|
|
self.shell_output_file = None
|
|
super().setUp()
|
|
|
|
def parse_output_json(self):
|
|
# Parses the outputted json.
|
|
with open(self.shell_output_file) as fd:
|
|
return json.load(fd)
|
|
|
|
def generate_suite(self, suite_output_path, template_file):
|
|
"""Read the template file, substitute the `outputLocation` and rewrite to the file `suite_output_path`."""
|
|
|
|
with open(os.path.normpath(template_file), "r", encoding="utf8") as template_suite_fd:
|
|
suite = yaml.safe_load(template_suite_fd)
|
|
|
|
try:
|
|
os.remove(suite_output_path)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
suite["executor"]["config"]["shell_options"]["global_vars"]["TestData"][
|
|
"outputLocation"
|
|
] = self.shell_output_file
|
|
with open(os.path.normpath(suite_output_path), "w") as fd:
|
|
yaml.dump(suite, fd, default_flow_style=False)
|
|
|
|
def generate_suite_and_execute_resmoke(self, suite_template, resmoke_args):
|
|
"""Generates a resmoke suite with the appropriate `outputLocation` and runs resmoke against that suite with the `fixture_info` test. Input `resmoke_args` are appended to the run command."""
|
|
|
|
self.shell_output_file = f"{self.test_dir}/output.json"
|
|
try:
|
|
os.remove(self.shell_output_file)
|
|
except OSError:
|
|
pass
|
|
|
|
suite_file = f"{self.test_dir}/suite.yml"
|
|
self.generate_suite(suite_file, suite_template)
|
|
|
|
self.logger.info(
|
|
"Running test. Template suite: %s Rewritten suite: %s Resmoke Args: %s Test output file: %s.",
|
|
suite_template,
|
|
suite_file,
|
|
resmoke_args,
|
|
self.shell_output_file,
|
|
)
|
|
|
|
resmoke_process = core.programs.make_process(
|
|
self.logger,
|
|
[sys.executable, "buildscripts/resmoke.py", "run", f"--suites={suite_file}"]
|
|
+ resmoke_args,
|
|
)
|
|
resmoke_process.start()
|
|
|
|
return resmoke_process
|
|
|
|
def test_suite_set_parameters(self):
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml", []
|
|
).wait()
|
|
|
|
set_params = self.parse_output_json()
|
|
self.assertEqual("1", set_params["enableTestCommands"])
|
|
self.assertEqual("false", set_params["testingDiagnosticsEnabled"])
|
|
self.assertEqual("{'storage': 2}", set_params["logComponentVerbosity"])
|
|
|
|
def test_cli_set_parameters(self):
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
["""--mongodSetParameter={"enableFlowControl": false, "flowControlMaxSamples": 500}"""],
|
|
).wait()
|
|
|
|
set_params = self.parse_output_json()
|
|
self.assertEqual("1", set_params["enableTestCommands"])
|
|
self.assertEqual("false", set_params["enableFlowControl"])
|
|
self.assertEqual("500", set_params["flowControlMaxSamples"])
|
|
|
|
def test_override_set_parameters(self):
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
["""--mongodSetParameter={"testingDiagnosticsEnabled": true}"""],
|
|
).wait()
|
|
|
|
set_params = self.parse_output_json()
|
|
self.assertEqual("true", set_params["testingDiagnosticsEnabled"])
|
|
self.assertEqual("{'storage': 2}", set_params["logComponentVerbosity"])
|
|
|
|
def test_merge_cli_set_parameters(self):
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
[
|
|
"""--mongodSetParameter={"enableFlowControl": false}""",
|
|
"""--mongodSetParameter={"flowControlMaxSamples": 500}""",
|
|
],
|
|
).wait()
|
|
|
|
set_params = self.parse_output_json()
|
|
self.assertEqual("false", set_params["testingDiagnosticsEnabled"])
|
|
self.assertEqual("{'storage': 2}", set_params["logComponentVerbosity"])
|
|
self.assertEqual("false", set_params["enableFlowControl"])
|
|
self.assertEqual("500", set_params["flowControlMaxSamples"])
|
|
|
|
def test_merge_error_cli_set_parameters(self):
|
|
self.assertEqual(
|
|
2,
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
[
|
|
"""--mongodSetParameter={"enableFlowControl": false}""",
|
|
"""--mongodSetParameter={"enableFlowControl": true}""",
|
|
],
|
|
).wait(),
|
|
)
|
|
|
|
def test_mongos_set_parameter(self):
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters_sharding.yml",
|
|
[
|
|
"""--mongosSetParameter={"maxTimeMSForHedgedReads": 100}""",
|
|
"""--mongosSetParameter={"mongosShutdownTimeoutMillisForSignaledShutdown": 1000}""",
|
|
],
|
|
).wait()
|
|
|
|
set_params = self.parse_output_json()
|
|
self.assertEqual("100", set_params["maxTimeMSForHedgedReads"])
|
|
self.assertEqual("1000", set_params["mongosShutdownTimeoutMillisForSignaledShutdown"])
|
|
|
|
def test_merge_error_cli_mongos_set_parameter(self):
|
|
self.assertEqual(
|
|
2,
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters_sharding.yml",
|
|
[
|
|
"""--mongosSetParameter={"maxTimeMSForHedgedReads": 100}""",
|
|
"""--mongosSetParameter={"maxTimeMSForHedgedReads": 1000}""",
|
|
],
|
|
).wait(),
|
|
)
|
|
|
|
def test_allow_duplicate_set_parameter_values(self):
|
|
self.assertEqual(
|
|
0,
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
[
|
|
"""--mongodSetParameter={"enableFlowControl": false}""",
|
|
"""--mongodSetParameter={"enableFlowControl": false}""",
|
|
],
|
|
).wait(),
|
|
)
|
|
|
|
self.assertEqual(
|
|
0,
|
|
self.generate_suite_and_execute_resmoke(
|
|
f"{self.suites_root}/resmoke_selftest_set_parameters.yml",
|
|
[
|
|
"""--mongodSetParameter={"mirrorReads": {samplingRate: 1.0}}""",
|
|
"""--mongodSetParameter={"mirrorReads": {samplingRate: 1.0}}""",
|
|
],
|
|
).wait(),
|
|
)
|
|
|
|
|
|
class TestDiscovery(_ResmokeSelftest):
|
|
def setUp(self):
|
|
super(TestDiscovery, self).setUp()
|
|
self.output = io.StringIO()
|
|
handler = logging.StreamHandler(self.output)
|
|
handler.setFormatter(logging.Formatter(fmt="%(message)s"))
|
|
self.logger.addHandler(handler)
|
|
|
|
self.assertIn(
|
|
"featureFlagToaster",
|
|
get_all_feature_flags_turned_off_by_default(),
|
|
"TestDiscovery tests use featureFlagToaster with the assumption that it is turned off by default.",
|
|
)
|
|
|
|
with open(
|
|
"buildscripts/resmokeconfig/fully_disabled_feature_flags.yml", encoding="utf8"
|
|
) as fully_disabled_ffs:
|
|
self.assertIn(
|
|
"featureFlagFryer",
|
|
yaml.safe_load(fully_disabled_ffs),
|
|
"TestDiscovery tests use featureFlagFryer with the assumption that it is present in fully_disabled_feature_flags.yml.",
|
|
)
|
|
|
|
def execute_resmoke(self, resmoke_args):
|
|
resmoke_process = core.programs.make_process(
|
|
self.logger,
|
|
[sys.executable, "buildscripts/resmoke.py", "test-discovery"] + resmoke_args,
|
|
)
|
|
resmoke_process.start()
|
|
return resmoke_process
|
|
|
|
def test_exclude_fully_disabled(self):
|
|
self.execute_resmoke(
|
|
["--suite=buildscripts/tests/resmoke_end2end/suites/resmoke_jstest_tagged.yml"]
|
|
).wait()
|
|
self.assertIn(
|
|
"buildscripts/tests/resmoke_end2end/testfiles/tagged_with_disabled_feature.js",
|
|
self.output.getvalue(),
|
|
"tagged_with_disabled_feature.js should have been included in the discovered tests.",
|
|
)
|
|
self.assertNotIn(
|
|
"buildscripts/tests/resmoke_end2end/testfiles/tagged_with_fully_disabled_feature.js",
|
|
self.output.getvalue(),
|
|
"tagged_with_fully_disabled_feature.js should not have been included in the discovered tests.",
|
|
)
|
|
|
|
def test_include_fully_disabled(self):
|
|
self.execute_resmoke(
|
|
[
|
|
"--suite=buildscripts/tests/resmoke_end2end/suites/resmoke_jstest_tagged.yml",
|
|
"--includeFullyDisabledFeatureTests",
|
|
]
|
|
).wait()
|
|
self.assertIn(
|
|
"buildscripts/tests/resmoke_end2end/testfiles/tagged_with_fully_disabled_feature.js",
|
|
self.output.getvalue(),
|
|
"tagged_with_fully_disabled_feature.js should have been included in the discovered tests since --includeFullyDisabledFeatureTests is used.",
|
|
)
|
|
|
|
|
|
def execute_resmoke(resmoke_args: List[str], subcommand: str="run"):
|
|
return subprocess.run(
|
|
[sys.executable, "buildscripts/resmoke.py", subcommand] + resmoke_args,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
|
|
class TestExceptionExtraction(unittest.TestCase):
|
|
def test_resmoke_python_exception(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_failing_python.yml",
|
|
]
|
|
output = execute_resmoke(resmoke_args).stdout
|
|
|
|
expected = "The following tests failed (with exit code):\n buildscripts/tests/resmoke_end2end/failtestfiles/python_failure.py (1 DB Exception)\n [LAST Part of Exception]"
|
|
assert expected in output
|
|
|
|
def test_resmoke_javascript_exception(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_failing_javascript.yml",
|
|
]
|
|
output = execute_resmoke(resmoke_args).stdout
|
|
|
|
expected = "The following tests failed (with exit code):\n buildscripts/tests/resmoke_end2end/failtestfiles/js_failure.js (253 Failure executing JS file)\n uncaught exception: Error: [true] and [false] are not equal"
|
|
assert expected in output
|
|
|
|
def test_resmoke_fixture_error(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_fixture_error.yml",
|
|
]
|
|
output = execute_resmoke(resmoke_args).stdout
|
|
|
|
expected = "The following tests had errors:\n job0_fixture_setup_0\n Traceback (most recent call last):\n"
|
|
assert expected in output
|
|
|
|
def test_resmoke_hook_error(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_hook_error.yml",
|
|
]
|
|
output = execute_resmoke(resmoke_args).stdout
|
|
|
|
expected = "The following tests had errors:\n buildscripts/tests/resmoke_end2end/failtestfiles/js_failure.js\n Traceback (most recent call last):\n"
|
|
assert expected in output
|
|
|
|
|
|
class TestForceExcludedTest(unittest.TestCase):
|
|
def test_no_force_exclude(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_suite_with_excludes.yml",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/one.js",
|
|
]
|
|
|
|
result = execute_resmoke(resmoke_args)
|
|
|
|
expected = (
|
|
"Cannot run excluded test in suite config. Use '--force-excluded-tests' to override:"
|
|
)
|
|
assert expected in result.stdout
|
|
assert result.returncode == 1
|
|
|
|
def test_with_force_exclude(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_suite_with_excludes.yml",
|
|
"--force-excluded-tests",
|
|
"--dryRun",
|
|
"tests",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/one.js",
|
|
]
|
|
|
|
result = execute_resmoke(resmoke_args)
|
|
|
|
assert result.returncode == 0
|
|
|
|
|
|
class TestSetShellSeed(unittest.TestCase):
|
|
def execute_resmoke_and_get_seed(self, resmoke_args):
|
|
process = execute_resmoke(resmoke_args)
|
|
self.assertEqual(process.returncode, 0)
|
|
match = re.search("setting random seed: ([0-9]+)", process.stdout)
|
|
if not match:
|
|
self.fail(
|
|
"No random seed message found in resmoke output. Was the message changed or the test altered?"
|
|
)
|
|
|
|
return match.group(1)
|
|
|
|
def test_set_shell_seed(self):
|
|
test_seed = "5000"
|
|
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_set_shellseed.yml",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/random_with_seed.js",
|
|
f"--shellSeed={test_seed}",
|
|
]
|
|
|
|
seed = self.execute_resmoke_and_get_seed(resmoke_args)
|
|
|
|
self.assertEqual(
|
|
seed,
|
|
test_seed,
|
|
msg="The found random seed does not match the seed passed with the --shellSeed resmoke argument.",
|
|
)
|
|
|
|
def test_random_shell_seed(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_set_shellseed.yml",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/random_with_seed.js",
|
|
]
|
|
|
|
random_seeds = set()
|
|
|
|
for _ in range(10):
|
|
seed = self.execute_resmoke_and_get_seed(resmoke_args)
|
|
random_seeds.add(seed)
|
|
|
|
self.assertTrue(
|
|
len(random_seeds) > 1, msg="Resmoke generated the same random seed 10 times in a row."
|
|
)
|
|
|
|
|
|
# In resmoke we expect certain parts of the evergreen config to be a certain way
|
|
# These tests will fail if something is not as expected and also needs to change somewhere else in resmoke
|
|
class TestEvergreenYML(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.evg_conf = parse_evergreen_file("etc/evergreen.yml")
|
|
config.CONFIG_DIR = "buildscripts/resmokeconfig"
|
|
|
|
def validate_jstestfuzz_selector(self, suite_names):
|
|
for suite_name in suite_names:
|
|
if not suite_name.startswith(
|
|
"//"
|
|
): # Ignore suites that are run via bazel. TODO: SERVER-104460
|
|
suite_config = suitesconfig.get_suite(suite_name).get_config()
|
|
expected_selector = ["jstestfuzz/out/*.js"]
|
|
self.assertEqual(
|
|
suite_config["selector"]["roots"],
|
|
expected_selector,
|
|
msg=f"The jstestfuzz selector for {suite_name} did not match 'jstestfuzz/out/*.js'",
|
|
)
|
|
|
|
# This test asserts that the jstestfuzz tasks uploads the the URL we expect it to
|
|
# If the remote url changes, also change it in the _log_local_resmoke_invocation method
|
|
# before fixing this test to the correct url
|
|
def test_jstestfuzz_download_url(self):
|
|
functions = self.evg_conf.functions
|
|
run_jstestfuzz = functions["run jstestfuzz"]
|
|
contains_correct_url = False
|
|
for item in run_jstestfuzz:
|
|
if item["command"] != "s3.put":
|
|
continue
|
|
|
|
remote_url = item["params"]["remote_file"]
|
|
if (
|
|
remote_url
|
|
== "${project}/${build_variant}/${revision}/jstestfuzz/${task_id}-${execution}.tgz"
|
|
):
|
|
contains_correct_url = True
|
|
break
|
|
|
|
self.assertTrue(
|
|
contains_correct_url,
|
|
msg="The 'run jstestfuzz' function in evergreen did not contain the remote_url that was expected",
|
|
)
|
|
|
|
# This tasks asserts that the way implicit multiversion tasks are defined has not changed
|
|
# If this fails, you will need to correct the _log_local_resmoke_invocation method before fixing
|
|
# this test
|
|
def test_implicit_multiversion_tasks(self):
|
|
multiverson_task_names = self.evg_conf.get_task_names_by_tag("multiversion")
|
|
implicit_multiversion_count = 0
|
|
for multiversion_task_name in multiverson_task_names:
|
|
task_config = self.evg_conf.get_task(multiversion_task_name)
|
|
func = task_config.find_func_command("initialize multiversion tasks")
|
|
if func is not None:
|
|
implicit_multiversion_count += 1
|
|
|
|
self.assertNotEqual(
|
|
0,
|
|
implicit_multiversion_count,
|
|
msg="Could not find any implicit multiversion tasks in evergreen",
|
|
)
|
|
|
|
# This tasks asserts that the way jstestfuzz tasks are defined has not changed
|
|
# It also asserts that the selector for jstestfuzz tasks always points to jstestfuzz/out/*.js
|
|
# If this fails, you will need to correct the _log_local_resmoke_invocation method before fixing
|
|
# this test
|
|
def test_jstestfuzz_tasks(self):
|
|
jstestfuzz_count = 0
|
|
for task in self.evg_conf.tasks:
|
|
generate_func = task.find_func_command("generate resmoke tasks")
|
|
if (
|
|
generate_func is None
|
|
or get_dict_value(generate_func, ["vars", "is_jstestfuzz"]) is not True
|
|
):
|
|
continue
|
|
|
|
jstestfuzz_count += 1
|
|
|
|
self.validate_jstestfuzz_selector(task.get_suite_names())
|
|
|
|
self.assertNotEqual(0, jstestfuzz_count, msg="Could not find any jstestfuzz tasks")
|
|
|
|
# our unittest names need to be correct for the spawnhost script to work.
|
|
# If you change the unittest names please also change them in the spawnhost script.
|
|
def test_unittest_name(self):
|
|
tasks = self.evg_conf.tasks
|
|
unit_test_string = "unit_test_group"
|
|
unit_test_tasks = []
|
|
for task in tasks:
|
|
# We found at least one unit test task that matched!
|
|
if unit_test_string in task.name:
|
|
unit_test_tasks.append(task.name)
|
|
|
|
# as of the time of writing this there are 16 different "unit_test_group" task definitions
|
|
self.assertGreaterEqual(
|
|
len(unit_test_tasks),
|
|
16,
|
|
"Something changed about unittest tasks, please make sure the spawnhost is also updated and update this test after.",
|
|
)
|
|
|
|
|
|
class TestMultiversionConfig(unittest.TestCase):
|
|
def test_valid_yaml(self):
|
|
file_name = "multiversion-config.yml"
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"buildscripts/resmoke.py",
|
|
"multiversion-config",
|
|
"--config-file-output",
|
|
file_name,
|
|
],
|
|
check=True,
|
|
)
|
|
with open(file_name, "r", encoding="utf8") as file:
|
|
file_contents = file.read()
|
|
|
|
try:
|
|
yaml.safe_load(file_contents)
|
|
except Exception:
|
|
self.fail(msg="`resmoke.py multiversion-config` does not output valid yaml.")
|
|
|
|
os.remove(file_name)
|
|
|
|
|
|
class TestCoreAnalyzerFunctions(unittest.TestCase):
|
|
def test_generated_task_name(self):
|
|
task_name = "test_tast_name"
|
|
execution = "0"
|
|
generated_task_name = get_generated_task_name(task_name, execution)
|
|
self.assertEquals(matches_generated_task_pattern(task_name, generated_task_name), execution)
|
|
self.assertIsNone(matches_generated_task_pattern("not_same_task", generated_task_name))
|
|
|
|
|
|
class TestValidateCollections(unittest.TestCase):
|
|
def test_validate_collections_passing(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_validate_collections.yml",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/validatecollections/test_pass.js",
|
|
]
|
|
|
|
result = execute_resmoke(resmoke_args)
|
|
|
|
expected = "Collection validation passed on collection test_validate_passes"
|
|
# Both nodes in the replica set should be checked and pass
|
|
self.assertEqual(result.stdout.count(expected), 2)
|
|
self.assertEqual(result.returncode, 0)
|
|
|
|
def test_validate_collections_failing(self):
|
|
resmoke_args = [
|
|
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_selftest_validate_collections.yml",
|
|
"buildscripts/tests/resmoke_end2end/testfiles/validatecollections/test_fail.js",
|
|
]
|
|
|
|
result = execute_resmoke(resmoke_args)
|
|
|
|
expected = "collection validation failed"
|
|
self.assertIn(expected, result.stdout)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
|
|
class TestModules(unittest.TestCase):
|
|
def test_files_included(self):
|
|
# this suite uses a fixture and hook from the module so it will fail if they are not loaded
|
|
# it also uses a
|
|
resmoke_args = [
|
|
"--resmokeModulesPath=buildscripts/tests/resmoke_end2end/test_resmoke_modules.yml",
|
|
"--suite=resmoke_test_module_worked",
|
|
]
|
|
|
|
result = execute_resmoke(resmoke_args)
|
|
self.assertEqual(result.returncode, 0)
|
|
|
|
def test_jstests_excluded(self):
|
|
# this first command should not include any of the tests from the module
|
|
resmoke_args = [
|
|
"--resmokeModulesPath=buildscripts/tests/resmoke_end2end/test_resmoke_modules.yml",
|
|
"--modules=none",
|
|
"--suite=buildscripts/tests/resmoke_end2end/suites/resmoke_test_module_jstests.yml",
|
|
"--dryRun=included-tests",
|
|
]
|
|
|
|
result_without_module = execute_resmoke(resmoke_args)
|
|
self.assertEqual(result_without_module.returncode, 0)
|
|
|
|
# this second invocartion should include all of the base jstests and all of the module jstests.
|
|
resmoke_args = [
|
|
"--resmokeModulesPath=buildscripts/tests/resmoke_end2end/test_resmoke_modules.yml",
|
|
"--modules=default",
|
|
"--suite=buildscripts/tests/resmoke_end2end/suites/resmoke_test_module_jstests.yml",
|
|
"--dryRun=included-tests",
|
|
]
|
|
|
|
result_with_module = execute_resmoke(resmoke_args)
|
|
self.assertEqual(result_with_module.returncode, 0)
|
|
|
|
# assert the test is in the list of tests when the module is included
|
|
self.assertIn("buildscripts/tests/resmoke_end2end/testfiles/one.js", result_with_module.stdout)
|
|
# assert the test is not in the list of tests when the module is excluded
|
|
self.assertNotIn("buildscripts/tests/resmoke_end2end/testfiles/one.js", result_without_module.stdout)
|