diff --git a/.evergreen/scripts/run_server.py b/.evergreen/scripts/run_server.py index 9757eb3a4f..f81964f367 100644 --- a/.evergreen/scripts/run_server.py +++ b/.evergreen/scripts/run_server.py @@ -1,6 +1,9 @@ from __future__ import annotations import os +import subprocess +import sys +from pathlib import Path from typing import Any from utils import DRIVERS_TOOLS, ROOT, get_test_options, run_command @@ -11,6 +14,28 @@ def set_env(name: str, value: Any = "1") -> None: def start_server(): + run_mongodb_script = ( + Path(DRIVERS_TOOLS) / ".evergreen" / "run-mongodb.sh" if DRIVERS_TOOLS else None + ) + want_help = bool({"-h", "--help"} & set(sys.argv[1:])) + if want_help and run_mongodb_script and run_mongodb_script.is_file(): + # Forward straight to run-mongodb.sh's own help, without run_command's + # "Running command..." logging noise. + subprocess.run( # noqa: S603 + ["bash", str(run_mongodb_script), "start", "-h"], # noqa: S607 + cwd=DRIVERS_TOOLS, + check=True, + ) + return + + # DRIVERS_TOOLS is only needed to actually start a server. When it's unset and + # -h/--help was requested, fall through to get_test_options' own argparse help below. + if not want_help and not DRIVERS_TOOLS: + raise ValueError( + "DRIVERS_TOOLS is not set; run `just run-server` from an Evergreen task " + "or set DRIVERS_TOOLS to a drivers-evergreen-tools checkout." + ) + opts, extra_opts = get_test_options( "Run a MongoDB server. All given flags will be passed to run-mongodb.sh in DRIVERS_TOOLS.", require_sub_test_name=False, diff --git a/.evergreen/scripts/utils.py b/.evergreen/scripts/utils.py index 0c718af899..aeff490962 100644 --- a/.evergreen/scripts/utils.py +++ b/.evergreen/scripts/utils.py @@ -48,8 +48,37 @@ class Distro: "numpy": "", } -# Tests that require a sub test suite. -SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"] +# Tests that require a sub test suite, mapped to their valid sub_test_name +# values, for test names where they're fully enumerable. A value of None +# means the sub_test_name is required but not restricted to a fixed set. +SUB_TEST_NAME_MAP: dict[str, list[str] | None] = { + "auth_aws": [ + "regular", + "assume-role", + "ec2", + "env-creds", + "session-creds", + "web-identity", + "ecs", + "ecs-remote", + ], + "kms": ["azure", "azure-remote", "azure-fail", "gcp", "gcp-remote", "gcp-fail"], + "mod_wsgi": ["standalone", "embedded"], + "perf": ["sync", "async"], + "auth_oidc": [ + "default", + "azure", + "azure-remote", + "gcp", + "gcp-remote", + "aks", + "aks-remote", + "gke", + "gke-remote", + "eks", + "eks-remote", + ], +} EXTRA_TESTS = ["mod_wsgi", "aws_lambda", "doctest"] @@ -66,31 +95,85 @@ class Distro: # Mapping of env variables to options OPTION_TO_ENV_VAR = {"cov": "COVERAGE", "crypt_shared": "TEST_CRYPT_SHARED"} +# Options that consume a following value, so it isn't mistaken for the test_name +# positional when inferring known_test_name below. +OPTIONS_WITH_VALUES = ["--green-framework", "--compressor", "--mongodb-api-version"] + + +def _first_positional_arg(argv: list[str]) -> str | None: + args = iter(argv) + for arg in args: + if arg in OPTIONS_WITH_VALUES: + next(args, None) + elif not arg.startswith("-"): + return arg + return None + def get_test_options( description, require_sub_test_name=True, allow_extra_opts=False ) -> tuple[argparse.Namespace, list[str]]: + # If a test name with known sub_test_name choices was given, pin the test_name + # argument to it so the usage/help output isn't cluttered with every choice. + known_test_name = None + sub_test_choices = None + if require_sub_test_name: + first_positional = _first_positional_arg(sys.argv[1:]) + if first_positional in SUB_TEST_NAME_MAP: + known_test_name = first_positional + sub_test_choices = SUB_TEST_NAME_MAP[known_test_name] + description = f"{description.rstrip('.')} for '{known_test_name}'." + parser = argparse.ArgumentParser( description=description, formatter_class=argparse.RawDescriptionHelpFormatter ) if require_sub_test_name: + if known_test_name: + parser.prog = f"{parser.prog} {known_test_name}" + test_name_choices = [known_test_name] + test_name_help = argparse.SUPPRESS + else: + test_name_choices = sorted(list(TEST_SUITE_MAP) + EXTRA_TESTS) + test_name_help = ( + "The optional name of the test suite to set up, typically the same name " + f"as a pytest marker. One of: {', '.join(test_name_choices)}." + ) parser.add_argument( "test_name", - choices=sorted(list(TEST_SUITE_MAP) + EXTRA_TESTS), + choices=test_name_choices, nargs="?", default="default", - help="The optional name of the test suite to set up, typically the same name as a pytest marker.", + metavar=known_test_name or "test_name", + help=test_name_help, ) + if known_test_name: + example_sub_test_name = sub_test_choices[0] if sub_test_choices else "azure" + help_text = f"The sub test name, for example {example_sub_test_name!r}. Required for {known_test_name!r}." + if sub_test_choices: + help_text += f" One of: {', '.join(sub_test_choices)}." + else: + help_text = "The optional sub test name, for example 'azure'." parser.add_argument( - "sub_test_name", nargs="?", help="The optional sub test name, for example 'azure'." + "sub_test_name", + nargs="?", + choices=sub_test_choices, + metavar="sub_test_name", + help=help_text, ) else: + run_server_choices = sorted( + set(list(TEST_SUITE_MAP) + EXTRA_TESTS) - set(NO_RUN_ORCHESTRATION) + ) parser.add_argument( "test_name", - choices=set(list(TEST_SUITE_MAP) + EXTRA_TESTS) - set(NO_RUN_ORCHESTRATION), + choices=run_server_choices, nargs="?", default="default", - help="The optional name of the test suite to be run, which informs the server configuration.", + metavar="test_name", + help=( + "The optional name of the test suite to be run, which informs the server " + f"configuration. One of: {', '.join(run_server_choices)}." + ), ) parser.add_argument( "--verbose", "-v", action="store_true", help="Whether to log at the DEBUG level." @@ -100,7 +183,9 @@ def get_test_options( ) parser.add_argument("--auth", action="store_true", help="Whether to add authentication.") parser.add_argument("--ssl", action="store_true", help="Whether to add TLS configuration.") - parser.add_argument( + + other_group = parser.add_argument_group("other options") + other_group.add_argument( "--test-min-deps", action="store_true", help="Test against minimum dependency versions" ) @@ -110,24 +195,26 @@ def get_test_options( "--debug-log", action="store_true", help="Enable pymongo standard logging." ) parser.add_argument("--cov", action="store_true", help="Add test coverage.") - parser.add_argument( + other_group.add_argument( "--green-framework", nargs=1, choices=["gevent"], help="Optional green framework to test against.", ) - parser.add_argument( + other_group.add_argument( "--compressor", nargs=1, choices=["zlib", "zstd", "snappy"], help="Optional compression algorithm.", ) - parser.add_argument("--crypt-shared", action="store_true", help="Test with crypt_shared.") - parser.add_argument("--no-ext", action="store_true", help="Run without c extensions.") - parser.add_argument( + other_group.add_argument( + "--crypt-shared", action="store_true", help="Test with crypt_shared." + ) + other_group.add_argument("--no-ext", action="store_true", help="Run without c extensions.") + other_group.add_argument( "--mongodb-api-version", choices=["1"], help="MongoDB stable API version to use." ) - parser.add_argument( + other_group.add_argument( "--disable-test-commands", action="store_true", help="Disable test commands." ) @@ -146,7 +233,7 @@ def get_test_options( # Handle validation and environment variable overrides. test_name = opts.test_name sub_test_name = opts.sub_test_name if require_sub_test_name else "" - if require_sub_test_name and test_name in SUB_TEST_REQUIRED and not sub_test_name: + if require_sub_test_name and test_name in SUB_TEST_NAME_MAP and not sub_test_name: raise ValueError(f"Test '{test_name}' requires a sub_test_name") handle_env_overrides(parser, opts) if "auth" in test_name: