diff options
author | David Bradford <david.bradford@mongodb.com> | 2019-07-16 16:34:29 -0400 |
---|---|---|
committer | David Bradford <david.bradford@mongodb.com> | 2019-07-24 11:53:10 -0400 |
commit | e324999f4444483c01fee1a7233bcfd31df2aed0 (patch) | |
tree | 584fa4328d5175304dfdf81c116bb23b49628dbf | |
parent | 65fb7862b698454fcb2cc6e791244a873ab1ce58 (diff) | |
download | mongo-e324999f4444483c01fee1a7233bcfd31df2aed0.tar.gz |
SERVER-42377: Update how burn_in_tests determines tests to run.
-rw-r--r-- | buildscripts/burn_in_tests.py | 146 | ||||
-rwxr-xr-x | buildscripts/bypass_compile_and_fetch_binaries.py | 72 | ||||
-rw-r--r-- | buildscripts/linter/git.py | 2 | ||||
-rw-r--r-- | buildscripts/linter/git_base.py (renamed from buildscripts/git.py) | 2 | ||||
-rw-r--r-- | buildscripts/patch_builds/__init__.py | 1 | ||||
-rw-r--r-- | buildscripts/patch_builds/change_data.py | 51 | ||||
-rw-r--r-- | buildscripts/tests/test_burn_in_tests.py | 403 | ||||
-rw-r--r-- | buildscripts/tests/test_git.py | 96 | ||||
-rw-r--r-- | etc/evergreen.yml | 64 | ||||
-rw-r--r-- | etc/pip/components/core.req | 4 | ||||
-rw-r--r-- | etc/pip/components/evergreen.req | 3 | ||||
-rw-r--r-- | etc/pip/components/resmoke.req | 2 | ||||
-rw-r--r-- | etc/pip/dev-requirements.txt | 1 | ||||
-rw-r--r-- | etc/pip/toolchain-requirements.txt | 1 |
14 files changed, 222 insertions, 626 deletions
diff --git a/buildscripts/burn_in_tests.py b/buildscripts/burn_in_tests.py index a0e9489048e..8e510c57ceb 100644 --- a/buildscripts/burn_in_tests.py +++ b/buildscripts/burn_in_tests.py @@ -12,7 +12,9 @@ import shlex import sys import urllib.parse -import requests +from git import Repo +import structlog +from structlog.stdlib import LoggerFactory import yaml from shrub.config import Configuration @@ -26,16 +28,22 @@ if __name__ == "__main__" and __package__ is None: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # pylint: disable=wrong-import-position -from buildscripts import git +from buildscripts.patch_builds.change_data import find_changed_files from buildscripts import resmokelib from buildscripts.ciconfig import evergreen -from buildscripts.client import evergreen as evergreen_client # pylint: enable=wrong-import-position -LOGGER = logging.getLogger(__name__) - -API_REST_PREFIX = "/rest/v1/" -API_SERVER_DEFAULT = "https://evergreen.mongodb.com" +structlog.configure(logger_factory=LoggerFactory()) +LOGGER = structlog.getLogger(__name__) +EXTERNAL_LOGGERS = { + "evergreen", + "git", + "urllib3", +} + +AVG_TEST_RUNTIME_ANALYSIS_DAYS = 14 +AVG_TEST_TIME_MULTIPLIER = 3 +CONFIG_FILE = ".evergreen.yml" REPEAT_SUITES = 2 EVERGREEN_FILE = "etc/evergreen.yml" MAX_TASKS_TO_CREATE = 1000 @@ -170,100 +178,37 @@ def validate_options(parser, options): check_variant(options.run_buildvariant, parser) -def find_last_activated_task(revisions, variant, branch_name): - """Get the git hash of the most recently activated build before this one.""" - - project = "mongodb-mongo-" + branch_name - build_prefix = "mongodb_mongo_" + branch_name + "_" + variant.replace("-", "_") - - evg_cfg = evergreen_client.read_evg_config() - if evg_cfg is not None and "api_server_host" in evg_cfg: - api_server = "{url.scheme}://{url.netloc}".format( - url=urllib.parse.urlparse(evg_cfg["api_server_host"])) - else: - api_server = API_SERVER_DEFAULT - - api_prefix = api_server + API_REST_PREFIX +def _is_file_a_test_file(file_path): + """ + Check if the given path points to a test file. - for githash in revisions: - url = "{}projects/{}/revisions/{}".format(api_prefix, project, githash) - response = requests.get(url) - revision_data = response.json() + :param file_path: path to file. + :return: True if path points to test. + """ + # Check that the file exists because it may have been moved or deleted in the patch. + if os.path.splitext(file_path)[1] != ".js" or not os.path.isfile(file_path): + return False - try: - for build in revision_data["builds"]: - if build.startswith(build_prefix): - url = "{}builds/{}".format(api_prefix, build) - build_resp = requests.get(url) - build_data = build_resp.json() - if build_data["activated"]: - return build_data["revision"] - except: # pylint: disable=bare-except - # Sometimes build data is incomplete, as was the related build. - pass + if "jstests" not in file_path: + return False - return None + return True -def find_changed_tests( # pylint: disable=too-many-locals - branch_name, base_commit, max_revisions, buildvariant, check_evergreen): - """Find the changed tests. +def find_changed_tests(repo: Repo): + """ + Find the changed tests. Use git to find which files have changed in this patch. TODO: This should be expanded to search for enterprise modules. The returned file paths are in normalized form (see os.path.normpath(path)). - """ - changed_tests = [] - - repo = git.Repository(".") - - if base_commit is None: - base_commit = repo.get_merge_base([branch_name + "@{upstream}", "HEAD"]) - - if check_evergreen: - # We're going to check up to 200 commits in Evergreen for the last scheduled one. - # The current commit will be activated in Evergreen; we use --skip to start at the - # previous commit when trying to find the most recent preceding commit that has been - # activated. - revs_to_check = repo.git_rev_list([base_commit, "--max-count=200", "--skip=1"]).splitlines() - last_activated = find_last_activated_task(revs_to_check, buildvariant, branch_name) - if last_activated is None: - # When the current commit is the first time 'buildvariant' has run, there won't be a - # commit among 'revs_to_check' that's been activated in Evergreen. We handle this by - # only considering tests changed in the current commit. - last_activated = "HEAD" - print("Comparing current branch against", last_activated) - revisions = repo.git_rev_list([base_commit + "..." + last_activated]).splitlines() - base_commit = last_activated - else: - revisions = repo.git_rev_list([base_commit + "...HEAD"]).splitlines() - - revision_count = len(revisions) - if revision_count > max_revisions: - print(("There are too many revisions included ({}). This is likely because your base" - " branch is not {}. You can allow us to review more than {} revisions by using" - " the --maxRevisions option.".format(revision_count, branch_name, max_revisions))) - return changed_tests - - changed_files = repo.git_diff(["--name-only", base_commit]).splitlines() - # New files ("untracked" in git terminology) won't show up in the git diff results. - untracked_files = repo.git_status(["--porcelain"]).splitlines() - - # The lines with untracked files start with '?? '. - for line in untracked_files: - if line.startswith("?"): - (_, line) = line.split(" ", 1) - changed_files.append(line) - - for line in changed_files: - line = line.rstrip() - # Check that the file exists because it may have been moved or deleted in the patch. - if os.path.splitext(line)[1] != ".js" or not os.path.isfile(line): - continue - if "jstests" in line: - path = os.path.normpath(line) - changed_tests.append(path) + :returns: Set of changed tests. + """ + changed_files = find_changed_files(repo) + LOGGER.debug("Found changed files", files=changed_files) + changed_tests = {os.path.normpath(path) for path in changed_files if _is_file_a_test_file(path)} + LOGGER.debug("Found changed tests", files=changed_tests) return changed_tests @@ -502,7 +447,7 @@ def create_generate_tasks_file(options, tests_by_task): json_config = evg_config.to_map() tasks_to_create = len(json_config.get('tasks', [])) if tasks_to_create > MAX_TASKS_TO_CREATE: - LOGGER.warning("Attempting to create more tasks than max(%d), aborting", tasks_to_create) + LOGGER.warning("Attempting to create more tasks than max, aborting", tasks=tasks_to_create) sys.exit(1) _write_json_file(json_config, options.generate_tasks_file) @@ -534,17 +479,23 @@ def run_tests(no_exec, tests_by_task, resmoke_cmd, report_file): _write_json_file(test_results, report_file) -def main(): - """Execute Main program.""" +def configure_logging(): + """Configure logging for the application.""" logging.basicConfig( format="[%(asctime)s - %(name)s - %(levelname)s] %(message)s", level=logging.DEBUG, stream=sys.stdout, ) + for log_name in EXTERNAL_LOGGERS: + logging.getLogger(log_name).setLevel(logging.WARNING) - options, args = parse_command_line() +def main(): + """Execute Main program.""" + + configure_logging() + options, args = parse_command_line() resmoke_cmd = _set_resmoke_cmd(options, args) # Load the dict of tests to run. @@ -561,9 +512,8 @@ def main(): # Parse the Evergreen project configuration file. evergreen_conf = evergreen.parse_evergreen_file(EVERGREEN_FILE) - changed_tests = find_changed_tests(options.branch, options.base_commit, - options.max_revisions, options.buildvariant, - options.check_evergreen) + repo = Repo(".") + changed_tests = find_changed_tests(repo) exclude_suites, exclude_tasks, exclude_tests = find_excludes(SELECTOR_FILE) changed_tests = filter_tests(changed_tests, exclude_tests) diff --git a/buildscripts/bypass_compile_and_fetch_binaries.py b/buildscripts/bypass_compile_and_fetch_binaries.py index f1a659710b4..976ae6f5742 100755 --- a/buildscripts/bypass_compile_and_fetch_binaries.py +++ b/buildscripts/bypass_compile_and_fetch_binaries.py @@ -3,6 +3,7 @@ import argparse import json +import logging import os import re import sys @@ -19,7 +20,10 @@ except ImportError: from urllib.parse import urlparse # type: ignore # pylint: enable=ungrouped-imports +from git.repo import Repo import requests +import structlog +from structlog.stdlib import LoggerFactory import yaml # Get relative imports to work when the package is not installed on the PYTHONPATH. @@ -28,9 +32,11 @@ if __name__ == "__main__" and __package__ is None: # pylint: disable=wrong-import-position from buildscripts.ciconfig.evergreen import parse_evergreen_file -from buildscripts.git import Repository # pylint: enable=wrong-import-position +structlog.configure(logger_factory=LoggerFactory()) +LOGGER = structlog.get_logger(__name__) + _IS_WINDOWS = (sys.platform == "win32" or sys.platform == "cygwin") # If changes are only from files in the bypass_files list or the bypass_directories list, then @@ -104,7 +110,7 @@ def requests_get_json(url): try: return response.json() except ValueError: - print("Invalid JSON object returned with response: {}".format(response.text)) + LOGGER.warning("Invalid JSON object returned with response", response=response.text) raise @@ -124,15 +130,16 @@ def read_evg_config(): def write_out_bypass_compile_expansions(patch_file, **expansions): """Write out the macro expansions to given file.""" with open(patch_file, "w") as out_file: - print("Saving compile bypass expansions to {0}: ({1})".format(patch_file, expansions)) + LOGGER.info("Saving compile bypass expansions", patch_file=patch_file, + expansions=expansions) yaml.safe_dump(expansions, out_file, default_flow_style=False) def write_out_artifacts(json_file, artifacts): """Write out the JSON file with URLs of artifacts to given file.""" with open(json_file, "w") as out_file: - print("Generating artifacts.json from pre-existing artifacts {0}".format( - json.dumps(artifacts, indent=4))) + LOGGER.info("Generating artifacts.json from pre-existing artifacts", json=json.dumps( + artifacts, indent=4)) json.dump(artifacts, out_file) @@ -182,8 +189,8 @@ def _get_original_etc_evergreen(path): :param path: path to etc/evergreen. :return: An EvergreenProjectConfig for the previous etc/evergreen file. """ - repo = Repository(".") - previous_contents = repo.git_show([f"HEAD:{path}"]) + repo = Repo(".") + previous_contents = repo.git.show([f"HEAD:{path}"]) with TemporaryDirectory() as tmpdir: file_path = os.path.join(tmpdir, "evergreen.yml") with open(file_path, "w") as fp: @@ -260,20 +267,18 @@ def should_bypass_compile(args): if os.path.isdir(filename): continue + log = LOGGER.bind(filename=filename) if _file_in_group(filename, BYPASS_BLACKLIST): - print("Compile bypass disabled after detecting {} as being modified because" - " it is a file known to affect compilation.".format(filename)) + log.warning("Compile bypass disabled due to blacklisted file") return False if not _file_in_group(filename, BYPASS_WHITELIST): - print("Compile bypass disabled after detecting {} as being modified because" - " it isn't a file known to not affect compilation.".format(filename)) + log.warning("Compile bypass disabled due to non-whitelisted file") return False if filename in BYPASS_EXTRA_CHECKS_REQUIRED: if not _check_file_for_bypass(filename, args.buildVariant): - print("Compile bypass disabled after detecting {} as being modified because" - " the changes could affect compilation.".format(filename)) + log.warning("Compile bypass disabled due to extra checks for file.") return False return True @@ -316,12 +321,18 @@ def main(): # pylint: disable=too-many-locals,too-many-statements determine to bypass compile do we write out the macro expansions. """ args = parse_args() + logging.basicConfig( + format="[%(asctime)s - %(name)s - %(levelname)s] %(message)s", + level=logging.DEBUG, + stream=sys.stdout, + ) # Determine if we should bypass compile based on modified patch files. if should_bypass_compile(args): evg_config = read_evg_config() if evg_config is None: - print("Could not find ~/.evergreen.yml config file. Default compile bypass to false.") + LOGGER.warning( + "Could not find ~/.evergreen.yml config file. Default compile bypass to false.") return api_server = "{url.scheme}://{url.netloc}".format( @@ -342,8 +353,8 @@ def main(): # pylint: disable=too-many-locals,too-many-statements if match: break else: - print("Could not find build id for revision {} on project {}." - " Default compile bypass to false.".format(args.revision, args.project)) + LOGGER.warning("Could not find build id. Default compile bypass to false.", + revision=args.revision, project=args.project) return # Generate the compile task id. @@ -353,23 +364,25 @@ def main(): # pylint: disable=too-many-locals,too-many-statements # Get info on compile task of base commit. task = requests_get_json(task_url) if task is None or task["status"] != "success": - print("Could not retrieve artifacts because the compile task {} for base commit" - " was not available. Default compile bypass to false.".format(compile_task_id)) + LOGGER.warning( + "Could not retrieve artifacts because the compile task for base commit" + " was not available. Default compile bypass to false.", task_id=compile_task_id) return # Get the compile task artifacts from REST API - print("Fetching pre-existing artifacts from compile task {}".format(compile_task_id)) + LOGGER.info("Fetching pre-existing artifacts from compile task", task_id=compile_task_id) artifacts = [] for artifact in task["files"]: filename = os.path.basename(artifact["url"]) if filename.startswith(build_id): - print("Retrieving archive {}".format(filename)) + LOGGER.info("Retrieving archive", filename=filename) # This is the artifacts.tgz as referenced in evergreen.yml. try: urllib.request.urlretrieve(artifact["url"], filename) except urllib.error.ContentTooShortError: - print("The artifact {} could not be completely downloaded. Default" - " compile bypass to false.".format(filename)) + LOGGER.warning( + "The artifact could not be completely downloaded. Default" + " compile bypass to false.", filename=filename) return # Need to extract certain files from the pre-existing artifacts.tgz. @@ -387,24 +400,25 @@ def main(): # pylint: disable=too-many-locals,too-many-statements tarinfo for tarinfo in tar.getmembers() if tarinfo.name.startswith("repo/") or tarinfo.name in extract_files ] - print("Extracting the following files from {0}...\n{1}".format( - filename, "\n".join(tarinfo.name for tarinfo in subdir))) + LOGGER.info("Extracting the files...", filename=filename, + files="\n".join(tarinfo.name for tarinfo in subdir)) tar.extractall(members=subdir) elif filename.startswith("mongo-src"): - print("Retrieving mongo source {}".format(filename)) + LOGGER.info("Retrieving mongo source", filename=filename) # This is the distsrc.[tgz|zip] as referenced in evergreen.yml. try: urllib.request.urlretrieve(artifact["url"], filename) except urllib.error.ContentTooShortError: - print("The artifact {} could not be completely downloaded. Default" - " compile bypass to false.".format(filename)) + LOGGER.warn( + "The artifact could not be completely downloaded. Default" + " compile bypass to false.", filename=filename) return extension = os.path.splitext(filename)[1] distsrc_filename = "distsrc{}".format(extension) - print("Renaming {} to {}".format(filename, distsrc_filename)) + LOGGER.info("Renaming", filename=filename, rename=distsrc_filename) os.rename(filename, distsrc_filename) else: - print("Linking base artifact {} to this patch build".format(filename)) + LOGGER.info("Linking base artifact to this patch build", filename=filename) # For other artifacts we just add their URLs to the JSON file to upload. files = { "name": artifact["name"], diff --git a/buildscripts/linter/git.py b/buildscripts/linter/git.py index 4fa15f65907..686f534987e 100644 --- a/buildscripts/linter/git.py +++ b/buildscripts/linter/git.py @@ -5,7 +5,7 @@ import os import re from typing import Any, Callable, List, Tuple -from buildscripts import git as _git +from buildscripts.linter import git_base as _git from buildscripts import moduleconfig from buildscripts.resmokelib.utils import globstar diff --git a/buildscripts/git.py b/buildscripts/linter/git_base.py index f2374f269d2..6af56ff29db 100644 --- a/buildscripts/git.py +++ b/buildscripts/linter/git_base.py @@ -1,8 +1,6 @@ """Module to run git commands on a repository.""" import logging -import os -import sys import subprocess LOGGER = logging.getLogger(__name__) diff --git a/buildscripts/patch_builds/__init__.py b/buildscripts/patch_builds/__init__.py new file mode 100644 index 00000000000..69ff837f7af --- /dev/null +++ b/buildscripts/patch_builds/__init__.py @@ -0,0 +1 @@ +"""Patch build module.""" diff --git a/buildscripts/patch_builds/change_data.py b/buildscripts/patch_builds/change_data.py new file mode 100644 index 00000000000..c8e1e321359 --- /dev/null +++ b/buildscripts/patch_builds/change_data.py @@ -0,0 +1,51 @@ +"""Tools for detecting changes in a commit.""" +from typing import Any, Set + +from git import Repo, DiffIndex +import structlog +from structlog.stdlib import LoggerFactory + +structlog.configure(logger_factory=LoggerFactory()) +LOGGER = structlog.get_logger(__name__) + + +def _paths_for_iter(diff, iter_type): + return {change.a_path for change in diff.iter_change_type(iter_type)} + + +def _modified_files_for_diff(diff: DiffIndex, log: Any) -> Set: + modified_files = _paths_for_iter(diff, 'M') + log.debug("modified files", files=modified_files) + + added_files = _paths_for_iter(diff, 'A') + log.debug("added files", files=added_files) + + renamed_files = _paths_for_iter(diff, 'R') + log.debug("renamed files", files=renamed_files) + + # We don't care about delete files, but log them just in case. + deleted_files = _paths_for_iter(diff, 'D') + log.debug("deleted files", files=deleted_files) + + return modified_files.union(added_files).union(renamed_files) + + +def find_changed_files(repo: Repo) -> Set[str]: + """ + Find files that were new or added to the repository between commits. + + :param repo: Git repository. + + :return: Set of changed files. + """ + diff = repo.index.diff(None) + work_tree_files = _modified_files_for_diff(diff, LOGGER.bind(diff="working tree diff")) + + commit = repo.index + diff = commit.diff(repo.head.commit) + index_files = _modified_files_for_diff(diff, LOGGER.bind(diff="index diff")) + + untracked_files = set(repo.untracked_files) + LOGGER.info("untracked files", files=untracked_files, diff="untracked diff") + + return work_tree_files.union(index_files).union(untracked_files) diff --git a/buildscripts/tests/test_burn_in_tests.py b/buildscripts/tests/test_burn_in_tests.py index afbfac59737..a9bbb03e73b 100644 --- a/buildscripts/tests/test_burn_in_tests.py +++ b/buildscripts/tests/test_burn_in_tests.py @@ -45,6 +45,13 @@ RUN_TESTS_MULTIVERSION_COMMAND = { "vars": {"resmoke_args": "--shellWriteMode=commands", "task_path_suffix": MULTIVERSION_PATH} } +NS = "buildscripts.burn_in_tests" + + +def ns(relative_name): # pylint: disable=invalid-name + """Return a full name from a name relative to the test module"s name space.""" + return NS + "." + relative_name + def tasks_mock( #pylint: disable=too-many-arguments tasks, generate_resmoke_tasks_command=None, get_vars_task_name=None, run_tests_command=None, @@ -701,97 +708,6 @@ class RunTests(unittest.TestCase): burn_in.run_tests(no_exec, TESTS_BY_TASK, resmoke_cmd, None) -class FindLastActivated(unittest.TestCase): - - REVISION_BUILDS = { - "rev1": { - "not_mongodb_mongo_master_variant1_build1": {"activated": False}, # force line break - "mongodb_mongo_unmaster_variant_build1": {"activated": True}, - "mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_master_variant2_build1": {"activated": False}, - "mongodb_mongo_master_variant3_build1": {"activated": False} - }, - "rev2": { - "not_mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_unmaster_variant_build1": {"activated": True}, - "mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_master_variant2_build1": {"activated": False} - }, - "rev3": { - "not_mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_unmaster_variant_build1": {"activated": True}, - "mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_master_variant2_build1": {"activated": False}, - }, - "rev4": { - "not_mongodb_mongo_master_variant1_build1": {"activated": True}, - "mongodb_mongo_unmaster_variant_build1": {"activated": True}, - "mongodb_mongo_master_variant1_build1": {"activated": True}, # force line break - "mongodb_mongo_master_variant2_build1": {"activated": False}, - "mongodb_mongo_master_variant3_build1": {"activated": True} - }, - } - - @staticmethod - def builds_url(build): - """Return build URL.""" - return "{}{}builds/{}".format(burn_in.API_SERVER_DEFAULT, burn_in.API_REST_PREFIX, build) - - @staticmethod - def revisions_url(project, revision): - """Return revisions URL.""" - return "{}{}projects/{}/revisions/{}".format(burn_in.API_SERVER_DEFAULT, - burn_in.API_REST_PREFIX, project, revision) - - @staticmethod - def load_urls(request, project, revision_builds): - """Store request in URLs to support REST APIs.""" - - for revision in revision_builds: - builds = revision_builds[revision] - # The 'revisions' endpoint contains the list of builds. - url = FindLastActivated.revisions_url(project, revision) - build_list = [] - for build in builds: - build_list.append("{}_{}".format(build, revision)) - build_data = {"builds": build_list} - request.put(url, None, build_data) - - for build in builds: - # The 'builds' endpoint contains the activated & revision field. - url = FindLastActivated.builds_url("{}_{}".format(build, revision)) - build_data = builds[build] - build_data["revision"] = revision - request.put(url, None, build_data) - - def _test_find_last_activated_task(self, branch, variant, revision, - revisions=REVISION_BUILDS.keys()): - with patch(BURN_IN + ".requests", MockRequests()),\ - patch(EVG_CLIENT + ".read_evg_config", return_value=None): - self.load_urls(burn_in.requests, "mongodb-mongo-master", self.REVISION_BUILDS) - last_revision = burn_in.find_last_activated_task(revisions, variant, branch) - self.assertEqual(last_revision, revision) - - def test_find_last_activated_task_first_rev(self): - self._test_find_last_activated_task("master", "variant1", "rev1") - - def test_find_last_activated_task_last_rev(self): - self._test_find_last_activated_task("master", "variant3", "rev4") - - def test_find_last_activated_task_no_rev(self): - self._test_find_last_activated_task("master", "variant2", None) - - def test_find_last_activated_task_no_variant(self): - self._test_find_last_activated_task("master", "novariant", None) - - def test_find_last_activated_task_no_branch(self): - with self.assertRaises(AttributeError): - self._test_find_last_activated_task("nobranch", "variant2", None) - - def test_find_last_activated_norevisions(self): - self._test_find_last_activated_task("master", "novariant", None, []) - - MEMBERS_MAP = { "test1.js": ["suite1", "suite2"], "test2.js": ["suite1", "suite3"], "test3.js": [], "test4.js": ["suite1", "suite2", "suite3"], "test5.js": ["suite2"] @@ -959,248 +875,63 @@ class CreateTaskList(unittest.TestCase): burn_in.create_task_list(EVERGREEN_CONF, variant, suite_list, []) -class FindChangedTests(unittest.TestCase): - - NUM_COMMITS = 10 - MOD_FILES = [os.path.normpath("jstests/test1.js"), os.path.normpath("jstests/test2.js")] - REV_DIFF = dict(zip([str(x) for x in range(NUM_COMMITS)], - [MOD_FILES] * NUM_COMMITS)) #type: ignore - NO_REV_DIFF = dict( - zip([str(x) for x in range(NUM_COMMITS)], [None for _ in range(NUM_COMMITS)])) - - UNTRACKED_FILES = [ - os.path.normpath("jstests/untracked1.js"), - os.path.normpath("jstests/untracked2.js") - ] - - @staticmethod - def _copy_rev_diff(rev_diff): - """Use this method instead of copy.deepcopy(). - - Note - it was discovered during testing that after using copy.deepcopy() that - updating one key would update all of them, i.e., - rev_diff = {"1": ["abc"], 2": ["abc"]} - copy_rev_diff = copy.deepcopy(rev_diff) - copy_rev_diff["2"] += "xyz" - print(rev_diff) - Result: {"1": ["abc"], 2": ["abc"]} - print(copy_rev_diff) - Result: {"1": ["abc", "xyz"], 2": ["abc", "xyz"]} - At this point no identifiable issue could be found related to this problem. - """ - copy_rev_diff = {} - for key in rev_diff: - copy_rev_diff[key] = [] - for file_name in rev_diff[key]: - copy_rev_diff[key].append(file_name) - return copy_rev_diff - - @staticmethod - def _get_rev_list(range1, range2): - return [str(num) for num in range(range1, range2 + 1)] - - def _mock_git_repository(self, directory): - return MockGitRepository(directory, FindChangedTests._get_rev_list(self.rev1, self.rev2), - self.rev_diff, self.untracked_files) - - def _test_find_changed_tests( #pylint: disable=too-many-arguments - self, commit, max_revisions, variant, check_evg, rev1, rev2, rev_diff, untracked_files, - last_activated_task=None): - branch = "master" - # pylint: disable=attribute-defined-outside-init - self.rev1 = rev1 - self.rev2 = rev2 - self.rev_diff = rev_diff - self.untracked_files = untracked_files - self.expected_changed_tests = [] - if commit is None and rev_diff: - self.expected_changed_tests += rev_diff[str(self.NUM_COMMITS - 1)] - elif rev_diff.get(commit, []): - self.expected_changed_tests += rev_diff.get(commit, []) - self.expected_changed_tests += untracked_files - # pylint: enable=attribute-defined-outside-init - with patch(EVG_CLIENT + ".read_evg_config", return_value=None),\ - patch(GIT + ".Repository", self._mock_git_repository),\ - patch("os.path.isfile", return_value=True),\ - patch(BURN_IN + ".find_last_activated_task", return_value=last_activated_task): - return burn_in.find_changed_tests(branch, commit, max_revisions, variant, check_evg) - - def test_find_changed_tests(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - - def test_find_changed_tests_no_changes(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, - self.NO_REV_DIFF, []) - self.assertEqual(changed_tests, []) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, - self.NO_REV_DIFF, [], "1") - self.assertEqual(changed_tests, []) - - def test_find_changed_tests_check_evergreen(self): - commit = "1" - rev_diff = self._copy_rev_diff(self.REV_DIFF) - rev_diff["2"] += [os.path.normpath("jstests/test.js")] - expected_changed_tests = self.REV_DIFF[commit] + self.UNTRACKED_FILES - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, rev_diff, - self.UNTRACKED_FILES) - self.assertEqual(changed_tests, expected_changed_tests) - rev_diff = self._copy_rev_diff(self.REV_DIFF) - rev_diff["3"] += [os.path.normpath("jstests/test.js")] - expected_changed_tests = rev_diff["3"] + self.UNTRACKED_FILES - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, rev_diff, - self.UNTRACKED_FILES, "1") - self.assertEqual(changed_tests, expected_changed_tests) - - def test_find_changed_tests_no_diff(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, - self.NO_REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.UNTRACKED_FILES) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, - self.NO_REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.UNTRACKED_FILES) - - def test_find_changed_tests_no_untracked(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, - self.REV_DIFF, []) - self.assertEqual(changed_tests, self.REV_DIFF[commit]) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, - self.REV_DIFF, []) - self.assertEqual(changed_tests, self.REV_DIFF[commit]) - - def test_find_changed_tests_no_base_commit(self): - changed_tests = self._test_find_changed_tests(None, 5, "myvariant", False, 0, 3, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - changed_tests = self._test_find_changed_tests(None, 5, "myvariant", True, 0, 3, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - - def test_find_changed_tests_non_js(self): - commit = "3" - rev_diff = self._copy_rev_diff(self.REV_DIFF) - rev_diff[commit] += [os.path.normpath("jstests/test.yml")] - untracked_files = self.UNTRACKED_FILES + [os.path.normpath("jstests/untracked.yml")] - expected_changed_tests = self.REV_DIFF[commit] + self.UNTRACKED_FILES - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, rev_diff, - untracked_files) - self.assertEqual(changed_tests, expected_changed_tests) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, rev_diff, - untracked_files) - self.assertEqual(changed_tests, expected_changed_tests) - - def test_find_changed_tests_not_in_jstests(self): - commit = "3" - rev_diff = self._copy_rev_diff(self.REV_DIFF) - rev_diff[commit] += [os.path.normpath("other/test.js")] - untracked_files = self.UNTRACKED_FILES + [os.path.normpath("other/untracked.js")] - expected_changed_tests = self.REV_DIFF[commit] + self.UNTRACKED_FILES - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 3, rev_diff, - untracked_files) - self.assertEqual(changed_tests, expected_changed_tests) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 3, rev_diff, - untracked_files) - self.assertEqual(changed_tests, expected_changed_tests) - - def test_find_changed_tests_no_revisions(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 0, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 0, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, self.expected_changed_tests) - - def test_find_changed_tests_too_many_revisions(self): - commit = "3" - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", False, 0, 9, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, []) - changed_tests = self._test_find_changed_tests(commit, 5, "myvariant", True, 0, 9, - self.REV_DIFF, self.UNTRACKED_FILES) - self.assertEqual(changed_tests, []) - - -class MockGitRepository(object): - def __init__(self, _, rev_list, rev_diff, untracked_files): - self.rev_list = rev_list - self.rev_diff = rev_diff - self.untracked_files = untracked_files - - def _get_revs(self, rev_range): - revs = rev_range.split("...") - if not revs: - return revs - elif len(revs) == 1: - revs.append("HEAD") - if revs[1] == "HEAD" and self.rev_list: - revs[1] = self.rev_list[-1] - return revs - - def __get_rev_range(self, rev_range): - commits = [] - if len(self.rev_list) < 2: - return commits - revs = self._get_revs(rev_range) - latest_commit_found = False - for commit in self.rev_list: - latest_commit_found = latest_commit_found or revs[0] == commit - if revs[1] == commit: - break - if latest_commit_found: - commits.append(commit) - return commits - - def get_merge_base(self, _): - return self.rev_list[-1] - - def git_rev_list(self, args): - return "\n".join(self.__get_rev_range(args[0])[::-1]) - - def git_diff(self, args): - revs = self._get_revs(args[1]) - if revs: - diff_list = self.rev_diff.get(revs[-1], []) - if diff_list: - return "\n".join(diff_list) - return "" - - def git_status(self, args): - revs = self._get_revs(args[0]) - modified_files = [""] - if revs: - diff_list = self.rev_diff.get(revs[-1], []) - if diff_list: - modified_files = [" M {}".format(untracked) for untracked in diff_list] - untracked_files = ["?? {}".format(untracked) for untracked in self.untracked_files] - return "\n".join(modified_files + untracked_files) - - -class MockResponse(object): - def __init__(self, response, json_data): - self.response = response - self.json_data = json_data - - def json(self): - return self.json_data - - -class MockRequests(object): - def __init__(self): - self.responses = {} - - def put(self, url, response, json_data): - self.responses[url] = MockResponse(response, json_data) - - def get(self, url): - if url in self.responses: - return self.responses[url] - return None +class TestFindChangedTests(unittest.TestCase): + @patch(ns("find_changed_files")) + def test_nothing_found(self, changed_files_mock): + repo_mock = MagicMock() + changed_files_mock.return_value = set() + + self.assertEqual(0, len(burn_in.find_changed_tests(repo_mock))) + + @patch(ns("find_changed_files")) + @patch(ns("os.path.isfile")) + def test_non_js_files_filtered(self, is_file_mock, changed_files_mock): + repo_mock = MagicMock() + file_list = [ + os.path.join("jstests", "test1.js"), + os.path.join("jstests", "test1.cpp"), + os.path.join("jstests", "test2.js"), + ] + changed_files_mock.return_value = set(file_list) + is_file_mock.return_value = True + + found_tests = burn_in.find_changed_tests(repo_mock) + + self.assertIn(file_list[0], found_tests) + self.assertIn(file_list[2], found_tests) + self.assertNotIn(file_list[1], found_tests) + + @patch(ns("find_changed_files")) + @patch(ns("os.path.isfile")) + def test_missing_files_filtered(self, is_file_mock, changed_files_mock): + repo_mock = MagicMock() + file_list = [ + os.path.join("jstests", "test1.js"), + os.path.join("jstests", "test2.js"), + os.path.join("jstests", "test3.js"), + ] + changed_files_mock.return_value = set(file_list) + is_file_mock.return_value = False + + found_tests = burn_in.find_changed_tests(repo_mock) + + self.assertEqual(0, len(found_tests)) + + @patch(ns("find_changed_files")) + @patch(ns("os.path.isfile")) + def test_non_jstests_files_filtered(self, is_file_mock, changed_files_mock): + repo_mock = MagicMock() + file_list = [ + os.path.join("jstests", "test1.js"), + os.path.join("other", "test2.js"), + os.path.join("jstests", "test3.js"), + ] + changed_files_mock.return_value = set(file_list) + is_file_mock.return_value = True + + found_tests = burn_in.find_changed_tests(repo_mock) + + self.assertIn(file_list[0], found_tests) + self.assertIn(file_list[2], found_tests) + self.assertNotIn(file_list[1], found_tests) + self.assertEqual(2, len(found_tests)) diff --git a/buildscripts/tests/test_git.py b/buildscripts/tests/test_git.py deleted file mode 100644 index c041fc6467b..00000000000 --- a/buildscripts/tests/test_git.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Unit tests for the buildscripts.git module.""" - -import subprocess -import unittest - -import buildscripts.git as _git - -# pylint: disable=missing-docstring,protected-access - - -class TestRepository(unittest.TestCase): - def setUp(self): - self.subprocess = MockSubprocess() - _git.subprocess = self.subprocess - - def tearDown(self): - _git.subprocess = subprocess - - def test_base_git_methods(self): - params = ["param1", "param2", "param3"] - repo = _git.Repository("/tmp") - self._check_gito_command(repo.git_add, "add", params) - self._check_gito_command(repo.git_commit, "commit", params) - self._check_gito_command(repo.git_diff, "diff", params) - self._check_gito_command(repo.git_log, "log", params) - self._check_gito_command(repo.git_push, "push", params) - self._check_gito_command(repo.git_fetch, "fetch", params) - self._check_gito_command(repo.git_ls_files, "ls-files", params) - self._check_gito_command(repo.git_rev_parse, "rev-parse", params) - self._check_gito_command(repo.git_rm, "rm", params) - self._check_gito_command(repo.git_show, "show", params) - self._check_gito_command(repo.git_status, "status", params) - - def test_base_gito_methods_errors(self): - params = ["param1", "param2", "param3"] - repo = _git.Repository("/tmp") - self._check_gito_command_error(repo.git_add, "add", params) - self._check_gito_command_error(repo.git_commit, "commit", params) - self._check_gito_command_error(repo.git_diff, "diff", params) - self._check_gito_command_error(repo.git_log, "log", params) - self._check_gito_command_error(repo.git_push, "push", params) - self._check_gito_command_error(repo.git_fetch, "fetch", params) - self._check_gito_command_error(repo.git_ls_files, "ls-files", params) - self._check_gito_command_error(repo.git_rev_parse, "rev-parse", params) - self._check_gito_command_error(repo.git_rm, "rm", params) - self._check_gito_command_error(repo.git_show, "show", params) - self._check_gito_command_error(repo.git_status, "status", params) - - def _check_gito_command(self, method, command, params): - # Initialize subprocess mock. - self.subprocess.call_output_args = None # pylint: disable=attribute-defined-outside-init - self.subprocess.call_output = str(method).encode("utf-8") - self.subprocess.call_returncode = 0 - # Call method. - value = method(params) - # Check. - args = self.subprocess.call_args - given_args = [command] + params - self.assertEqual("git", args[0]) - self.assertEqual(given_args, args[-len(given_args):]) - self.assertEqual(str(method), value) - - def _check_gito_command_error(self, method, command, params): - self.subprocess.call_args = None - self.subprocess.call_output = None - self.subprocess.call_returncode = 1 - - with self.assertRaises(_git.GitException): - method(params) - args = self.subprocess.call_args - given_args = [command] + params - self.assertEqual("git", args[0]) - self.assertEqual(given_args, args[-len(given_args):]) - - -class MockSubprocess(object): - PIPE = subprocess.PIPE - CalledProcessError = subprocess.CalledProcessError - - def __init__(self): - self.call_args = None - self.call_returncode = 0 - self.call_output = b"" - - def Popen(self, args, **kwargs): # pylint: disable=invalid-name,unused-argument - self.call_args = args - return MockProcess(self.call_returncode, self.call_output) - - -class MockProcess(object): - def __init__(self, returncode, output): - self.returncode = returncode - self._output = output - - def communicate(self): - return self._output, b"" diff --git a/etc/evergreen.yml b/etc/evergreen.yml index 2db1f42acf7..981696ee628 100644 --- a/etc/evergreen.yml +++ b/etc/evergreen.yml @@ -254,8 +254,8 @@ variables: # The python virtual environment is installed in ${workdir}, which is created in # "set up virtualenv". - func: "set up virtualenv" + - func: "configure evergreen api credentials" # NOTE: To disable the compile bypass feature, comment out the next line. - # - func: "bypass compile and fetch binaries" - func: "update bypass expansions" - func: "get buildnumber" @@ -1239,7 +1239,6 @@ functions: script: | # exit immediately if virtualenv is not found set -o errexit - set -o verbose virtualenv_loc=$(which ${virtualenv|virtualenv}) @@ -1481,7 +1480,6 @@ functions: working_dir: src script: | set -o errexit - set -o verbose ${activate_virtualenv} $python buildscripts/evergreen_generate_resmoke_tasks.py --expansion-file expansions.yml --verbose @@ -4716,75 +4714,19 @@ tasks: ${activate_virtualenv} find buildscripts etc jstests -name '*.y*ml' -exec yamllint -c etc/yamllint_config.yml {} + -### This task is deprecated, but left in here in case of need to run burn_in_tests -### instead of the generated task: -### - Rename burn_in_tests_gen task to burn_in_tests_gen_UNUSED -### - Rename burn_in_tests_gen to burn_in_tests on the build variants -- <<: *task_template - name: burn_in_tests - depends_on: - - name: compile - commands: - - command: manifest.load - - func: "git get project" - # The repository is cloned in a directory distinct from src for the modified test detection - # because the extraction of the artifacts performed in the 'do setup' causes - # 'git diff --name-only' to see all tests as modified on Windows (git 1.9.5). See SERVER-30634. - vars: - git_project_directory: burn_in_tests_clonedir - - func: "do setup" - - command: shell.exec - params: - working_dir: burn_in_tests_clonedir - shell: bash - script: | - set -o errexit - set -o verbose - # If this is a scheduled build, we check for changes against the last scheduled commit. - if [ "${is_patch}" != "true" ]; then - burn_in_args="--checkEvergreen" - fi - pushd ../src - ${activate_virtualenv} - popd - # Capture a list of new and modified tests. - build_variant=${build_variant} - if [ -n "${burn_in_tests_build_variant|}" ]; then - build_variant=${burn_in_tests_build_variant|} - fi - # Evergreen executable is in $HOME. - PATH=$PATH:$HOME $python buildscripts/burn_in_tests.py --branch=${branch_name} --buildVariant=$build_variant --testListOutfile=jstests/new_tests.json --noExec $burn_in_args - # Copy the results to the src dir. - cp jstests/new_tests.json ../src/jstests/new_tests.json - - func: "do multiversion setup" - - func: "run tests" - vars: - task_path_suffix: /data/multiversion:$HOME - resmoke_wrapper: $python buildscripts/burn_in_tests.py --testListFile=jstests/new_tests.json - resmoke_args: --repeatSuites=2 -### - - name: burn_in_tests_gen commands: - command: manifest.load - func: "git get project" - # The repository is cloned in a directory distinct from src for the modified test detection - # because the extraction of the artifacts performed in the 'do setup' causes - # 'git diff --name-only' to see all tests as modified on Windows (git 1.9.5). See SERVER-30634. - vars: - git_project_directory: burn_in_tests_clonedir - func: "set task expansion macros" - func: "set up virtualenv" - vars: - pip_dir: ${workdir}/burn_in_tests_clonedir/etc/pip - command: shell.exec params: - working_dir: burn_in_tests_clonedir + working_dir: src shell: bash script: | set -o errexit set -o verbose - mkdir ../src ${activate_virtualenv} # If this is a scheduled build, we check for changes against the last scheduled commit. if [ "${is_patch}" != "true" ]; then @@ -4800,7 +4742,7 @@ tasks: # Increase the burn_in repetition from 2 to 1000 executions or 10 minutes burn_in_args="$burn_in_args --repeatTestsMin=2 --repeatTestsMax=1000 --repeatTestsSecs=600" # Evergreen executable is in $HOME. - PATH=$PATH:$HOME $python buildscripts/burn_in_tests.py --branch=${branch_name} $build_variant_opts --distro=${distro_id} --generateTasksFile=../src/burn_in_tests_gen.json --noExec $burn_in_args + PATH=$PATH:$HOME $python buildscripts/burn_in_tests.py --branch=${branch_name} $build_variant_opts --distro=${distro_id} --generateTasksFile=burn_in_tests_gen.json --noExec $burn_in_args - command: archive.targz_pack params: target: src/burn_in_tests_gen.tgz diff --git a/etc/pip/components/core.req b/etc/pip/components/core.req index 549c6a764db..b82f6f6c748 100644 --- a/etc/pip/components/core.req +++ b/etc/pip/components/core.req @@ -1,5 +1,5 @@ # Core (we need these for most buildscripts) +psutil +pymongo >= 3.0, != 3.6.0 # See PYTHON-1434, SERVER-34820 PyYAML >= 3.0.0 requests >= 2.0.0 -pymongo >= 3.0, != 3.6.0 # See PYTHON-1434, SERVER-34820 -psutil diff --git a/etc/pip/components/evergreen.req b/etc/pip/components/evergreen.req index a4d92cc08db..b5ab0d8b133 100644 --- a/etc/pip/components/evergreen.req +++ b/etc/pip/components/evergreen.req @@ -1 +1,4 @@ +click ~= 7.0 +GitPython ~= 2.1.11 psutil +structlog ~= 19.1.0 diff --git a/etc/pip/components/resmoke.req b/etc/pip/components/resmoke.req index bd568f9fa85..8078533fdce 100644 --- a/etc/pip/components/resmoke.req +++ b/etc/pip/components/resmoke.req @@ -1,5 +1,5 @@ PyKMIP == 0.4.0 # It's now 0.8.0. We're far enough back to have API conflicts. -evergreen.py == 0.3.2 +evergreen.py == 0.3.9 jinja2 mock shrub.py == 0.2.0 diff --git a/etc/pip/dev-requirements.txt b/etc/pip/dev-requirements.txt index bd3471b61eb..71b3841a7a6 100644 --- a/etc/pip/dev-requirements.txt +++ b/etc/pip/dev-requirements.txt @@ -4,3 +4,4 @@ -r components/compile.req -r components/lint.req -r components/resmoke.req +-r components/evergreen.req diff --git a/etc/pip/toolchain-requirements.txt b/etc/pip/toolchain-requirements.txt index 53adbd7c0ff..099f43b4386 100644 --- a/etc/pip/toolchain-requirements.txt +++ b/etc/pip/toolchain-requirements.txt @@ -4,6 +4,7 @@ -r components/core.req -r components/compile.req +-r components/evergreen.req -r components/lint.req -r components/mypy.req -r components/resmoke.req |