diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 4562f1d2be..e5b640a890 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -227,17 +227,6 @@ functions: args: - ${DRIVERS_TOOLS}/.evergreen/atlas_data_lake/run-mongohouse-image.sh - "run mod_wsgi tests": - - command: subprocess.exec - type: test - params: - include_expansions_in_env: [MOD_WSGI_VERSION, MOD_WSGI_EMBEDDED, "PYTHON_BINARY"] - working_dir: "src" - binary: bash - args: - - .evergreen/scripts/run-with-env.sh - - .evergreen/scripts/run-mod-wsgi-tests.sh - "run doctests": - command: subprocess.exec type: test @@ -411,40 +400,6 @@ tasks: TEST_NAME: index_management AUTH: "auth" - - name: "mod-wsgi-standalone" - tags: ["mod_wsgi"] - commands: - - func: "run server" - vars: - TOPOLOGY: "server" - - func: "run mod_wsgi tests" - - - name: "mod-wsgi-replica-set" - tags: ["mod_wsgi"] - commands: - - func: "run server" - vars: - TOPOLOGY: "replica_set" - - func: "run mod_wsgi tests" - - - name: "mod-wsgi-embedded-mode-standalone" - tags: ["mod_wsgi"] - commands: - - func: "run server" - - func: "run mod_wsgi tests" - vars: - MOD_WSGI_EMBEDDED: "1" - - - name: "mod-wsgi-embedded-mode-replica-set" - tags: ["mod_wsgi"] - commands: - - func: "run server" - vars: - TOPOLOGY: "replica_set" - - func: "run mod_wsgi tests" - vars: - MOD_WSGI_EMBEDDED: "1" - - name: "no-server" tags: ["no-server"] commands: diff --git a/.evergreen/generated_configs/tasks.yml b/.evergreen/generated_configs/tasks.yml index 0b0f09329a..070b163e90 100644 --- a/.evergreen/generated_configs/tasks.yml +++ b/.evergreen/generated_configs/tasks.yml @@ -775,6 +775,48 @@ tasks: TEST_NAME: load_balancer tags: [load-balancer, noauth, nossl] + # Mod wsgi tests + - name: mod-wsgi-standalone + commands: + - func: run server + vars: + TOPOLOGY: standalone + - func: run tests + vars: + TEST_NAME: mod_wsgi + SUB_TEST_NAME: standalone + tags: [mod_wsgi] + - name: mod-wsgi-replica-set + commands: + - func: run server + vars: + TOPOLOGY: replica_set + - func: run tests + vars: + TEST_NAME: mod_wsgi + SUB_TEST_NAME: standalone + tags: [mod_wsgi] + - name: mod-wsgi-embedded-mode-standalone + commands: + - func: run server + vars: + TOPOLOGY: standalone + - func: run tests + vars: + TEST_NAME: mod_wsgi + SUB_TEST_NAME: embedded + tags: [mod_wsgi] + - name: mod-wsgi-embedded-mode-replica-set + commands: + - func: run server + vars: + TOPOLOGY: replica_set + - func: run tests + vars: + TEST_NAME: mod_wsgi + SUB_TEST_NAME: embedded + tags: [mod_wsgi] + # Ocsp tests - name: test-ocsp-ecdsa-valid-cert-server-does-not-staple commands: diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index 4c54abf4b9..d70afa2bdd 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -696,10 +696,7 @@ buildvariants: # Mod wsgi tests - name: mod_wsgi-ubuntu-22-python3.9 tasks: - - name: mod-wsgi-standalone - - name: mod-wsgi-replica-set - - name: mod-wsgi-embedded-mode-standalone - - name: mod-wsgi-embedded-mode-replica-set + - name: .mod_wsgi display_name: mod_wsgi Ubuntu-22 Python3.9 run_on: - ubuntu2204-small @@ -708,10 +705,7 @@ buildvariants: PYTHON_BINARY: /opt/python/3.9/bin/python3 - name: mod_wsgi-ubuntu-22-python3.13 tasks: - - name: mod-wsgi-standalone - - name: mod-wsgi-replica-set - - name: mod-wsgi-embedded-mode-standalone - - name: mod-wsgi-embedded-mode-replica-set + - name: .mod_wsgi display_name: mod_wsgi Ubuntu-22 Python3.13 run_on: - ubuntu2204-small diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index d91e0e6ded..b90a6af437 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -614,12 +614,7 @@ def create_atlas_data_lake_variants(): def create_mod_wsgi_variants(): variants = [] host = HOSTS["ubuntu22"] - tasks = [ - "mod-wsgi-standalone", - "mod-wsgi-replica-set", - "mod-wsgi-embedded-mode-standalone", - "mod-wsgi-embedded-mode-replica-set", - ] + tasks = [".mod_wsgi"] expansions = dict(MOD_WSGI_VERSION="4") for python in MIN_MAX_PYTHON: display_name = get_display_name("mod_wsgi", host, python=python) @@ -892,6 +887,24 @@ def create_oidc_tasks(): return tasks +def create_mod_wsgi_tasks(): + tasks = [] + for test, topology in product(["standalone", "embedded-mode"], ["standalone", "replica_set"]): + if test == "standalone": + task_name = "mod-wsgi-" + else: + task_name = "mod-wsgi-embedded-mode-" + task_name += topology.replace("_", "-") + server_vars = dict(TOPOLOGY=topology) + server_func = FunctionCall(func="run server", vars=server_vars) + vars = dict(TEST_NAME="mod_wsgi", SUB_TEST_NAME=test.split("-")[0]) + test_func = FunctionCall(func="run tests", vars=vars) + tags = ["mod_wsgi"] + commands = [server_func, test_func] + tasks.append(EvgTask(name=task_name, tags=tags, commands=commands)) + return tasks + + def _create_ocsp_task(algo, variant, server_type, base_task_name): file_name = f"{algo}-basic-tls-ocsp-{variant}.json" diff --git a/.evergreen/scripts/mod_wsgi_tester.py b/.evergreen/scripts/mod_wsgi_tester.py new file mode 100644 index 0000000000..5968849068 --- /dev/null +++ b/.evergreen/scripts/mod_wsgi_tester.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import os +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path +from shutil import which + +from utils import LOGGER, ROOT, run_command, write_env + + +def make_request(url, timeout=10): + for _ in range(int(timeout)): + try: + urllib.request.urlopen(url) # noqa: S310 + return + except urllib.error.HTTPError: + pass + time.sleep(1) + raise TimeoutError(f"Failed to access {url}") + + +def setup_mod_wsgi(sub_test_name: str) -> None: + env = os.environ.copy() + if sub_test_name == "embedded": + env["MOD_WSGI_CONF"] = "mod_wsgi_test_embedded.conf" + elif sub_test_name == "standalone": + env["MOD_WSGI_CONF"] = "mod_wsgi_test.conf" + else: + raise ValueError("mod_wsgi sub test must be either 'standalone' or 'embedded'") + write_env("MOD_WSGI_CONF", env["MOD_WSGI_CONF"]) + apache = which("apache2") + if not apache and Path("/usr/lib/apache2/mpm-prefork/apache2").exists(): + apache = "/usr/lib/apache2/mpm-prefork/apache2" + if apache: + apache_config = "apache24ubuntu161404.conf" + else: + apache = which("httpd") + if not apache: + raise ValueError("Could not find apache2 or httpd") + apache_config = "apache22amazon.conf" + python_version = ".".join(str(val) for val in sys.version_info[:2]) + mod_wsgi_version = 4 + so_file = f"/opt/python/mod_wsgi/python_version/{python_version}/mod_wsgi_version/{mod_wsgi_version}/mod_wsgi.so" + write_env("MOD_WSGI_SO", so_file) + env["MOD_WSGI_SO"] = so_file + env["PYTHONHOME"] = f"/opt/python/{python_version}" + env["PROJECT_DIRECTORY"] = project_directory = str(ROOT) + write_env("APACHE_BINARY", apache) + write_env("APACHE_CONFIG", apache_config) + uri1 = f"http://localhost:8080/interpreter1{project_directory}" + write_env("TEST_URI1", uri1) + uri2 = f"http://localhost:8080/interpreter2{project_directory}" + write_env("TEST_URI2", uri2) + run_command(f"{apache} -k start -f {ROOT}/test/mod_wsgi_test/{apache_config}", env=env) + + # Wait for the endpoints to be available. + try: + make_request(uri1, 10) + make_request(uri2, 10) + except Exception as e: + LOGGER.error(Path("error_log").read_text()) + raise e + + +def test_mod_wsgi() -> None: + sys.path.insert(0, ROOT) + from test.mod_wsgi_test.test_client import main, parse_args + + uri1 = os.environ["TEST_URI1"] + uri2 = os.environ["TEST_URI2"] + args = f"-n 25000 -t 100 parallel {uri1} {uri2}" + try: + main(*parse_args(args.split())) + + args = f"-n 25000 serial {uri1} {uri2}" + main(*parse_args(args.split())) + except Exception as e: + LOGGER.error(Path("error_log").read_text()) + raise e + + +def teardown_mod_wsgi() -> None: + apache = os.environ["APACHE_BINARY"] + apache_config = os.environ["APACHE_CONFIG"] + + run_command(f"{apache} -k stop -f {ROOT}/test/mod_wsgi_test/{apache_config}") + + +if __name__ == "__main__": + setup_mod_wsgi() diff --git a/.evergreen/scripts/run-mod-wsgi-tests.sh b/.evergreen/scripts/run-mod-wsgi-tests.sh deleted file mode 100755 index f59ace8116..0000000000 --- a/.evergreen/scripts/run-mod-wsgi-tests.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -o xtrace -set -o errexit - -APACHE=$(command -v apache2 || command -v /usr/lib/apache2/mpm-prefork/apache2) || true -if [ -n "$APACHE" ]; then - APACHE_CONFIG=apache24ubuntu161404.conf -else - APACHE=$(command -v httpd) || true - if [ -z "$APACHE" ]; then - echo "Could not find apache2 binary" - exit 1 - else - APACHE_CONFIG=apache22amazon.conf - fi -fi - - -PYTHON_VERSION=$(${PYTHON_BINARY} -c "import sys; sys.stdout.write('.'.join(str(val) for val in sys.version_info[:2]))") - -# Ensure the C extensions are installed. -${PYTHON_BINARY} -m venv --system-site-packages .venv -source .venv/bin/activate -pip install -U pip -export PYMONGO_C_EXT_MUST_BUILD=1 -python -m pip install -v -e . - -export MOD_WSGI_SO=/opt/python/mod_wsgi/python_version/$PYTHON_VERSION/mod_wsgi_version/$MOD_WSGI_VERSION/mod_wsgi.so -export PYTHONHOME=/opt/python/$PYTHON_VERSION -# If MOD_WSGI_EMBEDDED is set use the default embedded mode behavior instead -# of daemon mode (WSGIDaemonProcess). -if [ -n "${MOD_WSGI_EMBEDDED:-}" ]; then - export MOD_WSGI_CONF=mod_wsgi_test_embedded.conf -else - export MOD_WSGI_CONF=mod_wsgi_test.conf -fi - -cd .. -$APACHE -k start -f ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${APACHE_CONFIG} -trap '$APACHE -k stop -f ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${APACHE_CONFIG}' EXIT HUP - -wget -t 1 -T 10 -O - "http://localhost:8080/interpreter1${PROJECT_DIRECTORY}" || (cat error_log && exit 1) -wget -t 1 -T 10 -O - "http://localhost:8080/interpreter2${PROJECT_DIRECTORY}" || (cat error_log && exit 1) - -python ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 -t 100 parallel \ - http://localhost:8080/interpreter1${PROJECT_DIRECTORY} http://localhost:8080/interpreter2${PROJECT_DIRECTORY} || \ - (tail -n 100 error_log && exit 1) - -python ${PROJECT_DIRECTORY}/test/mod_wsgi_test/test_client.py -n 25000 serial \ - http://localhost:8080/interpreter1${PROJECT_DIRECTORY} http://localhost:8080/interpreter2${PROJECT_DIRECTORY} || \ - (tail -n 100 error_log && exit 1) - -rm -rf .venv diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index 38fd3c67cb..13a510475f 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -100,6 +100,13 @@ def run() -> None: if TEST_PERF: start_time = datetime.now() + # Run mod_wsgi tests using the helper. + if TEST_NAME == "mod_wsgi": + from mod_wsgi_tester import test_mod_wsgi + + test_mod_wsgi() + return + # Send kms tests to run remotely. if TEST_NAME == "kms" and SUB_TEST_NAME in ["azure", "gcp"]: from kms_tester import test_kms_send_to_remote diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index 868ac419b5..fea397e642 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -251,6 +251,11 @@ def handle_test_env() -> None: cmd = f'bash "{DRIVERS_TOOLS}/.evergreen/run-load-balancer.sh" start' run_command(cmd) + if test_name == "mod_wsgi": + from mod_wsgi_tester import setup_mod_wsgi + + setup_mod_wsgi(sub_test_name) + if test_name == "ocsp": if sub_test_name: os.environ["OCSP_SERVER_TYPE"] = sub_test_name @@ -378,7 +383,7 @@ def handle_test_env() -> None: # Use --capture=tee-sys so pytest prints test output inline: # https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html TEST_ARGS = f"-v --capture=tee-sys --durations=5 {TEST_ARGS}" - TEST_SUITE = TEST_SUITE_MAP[test_name] + TEST_SUITE = TEST_SUITE_MAP.get(test_name) if TEST_SUITE: TEST_ARGS = f"-m {TEST_SUITE} {TEST_ARGS}" diff --git a/.evergreen/scripts/teardown_tests.py b/.evergreen/scripts/teardown_tests.py index 3920180422..750d2a0652 100644 --- a/.evergreen/scripts/teardown_tests.py +++ b/.evergreen/scripts/teardown_tests.py @@ -44,4 +44,10 @@ elif TEST_NAME == "auth_aws" and sys.platform != "darwin": run_command(f"bash {DRIVERS_TOOLS}/.evergreen/auth_aws/teardown.sh") +# Tear down mog_wsgi if applicable. +elif TEST_NAME == "mod_wsgi": + from mod_wsgi_tester import teardown_mod_wsgi + + teardown_mod_wsgi() + LOGGER.info(f"Tearing down tests of type '{TEST_NAME}'... done.") diff --git a/.evergreen/scripts/utils.py b/.evergreen/scripts/utils.py index 08d376461e..45d01ae6bd 100644 --- a/.evergreen/scripts/utils.py +++ b/.evergreen/scripts/utils.py @@ -50,7 +50,9 @@ class Distro: } # Tests that require a sub test suite. -SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms"] +SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi"] + +EXTRA_TESTS = ["mod_wsgi"] def get_test_options( @@ -62,7 +64,7 @@ def get_test_options( if require_sub_test_name: parser.add_argument( "test_name", - choices=sorted(TEST_SUITE_MAP), + choices=sorted(list(TEST_SUITE_MAP) + EXTRA_TESTS), nargs="?", default="default", help="The optional name of the test suite to set up, typically the same name as a pytest marker.", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8844565d31..d2a833d874 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -275,6 +275,17 @@ Note: these tests can only be run from an Evergreen host. - Run `just setup-tests atlas_connect`. - Run `just run-tests`. +### mod_wsgi tests + +Note: these tests can only be run from an Evergreen Linux host that has the Python toolchain. + +- Run `just run-server`. +- Run `just setup-tests mod_wsgi <mode>`. +- Run `just run-tests`. + +The `mode` can be `standalone` or `embedded`. For the `replica_set` version of the tests, use +`TOPOLOGY=replica_set just run-server`. + ### OCSP tests - Export the orchestration file, e.g. `export ORCHESTRATION_FILE=rsa-basic-tls-ocsp-disableStapling.json`. diff --git a/test/mod_wsgi_test/test_client.py b/test/mod_wsgi_test/test_client.py index 88eeb7a57e..c122863bfa 100644 --- a/test/mod_wsgi_test/test_client.py +++ b/test/mod_wsgi_test/test_client.py @@ -24,7 +24,7 @@ from urllib.request import urlopen -def parse_args(): +def parse_args(args=None): parser = OptionParser( """usage: %prog [options] mode url [<url2>...] @@ -70,7 +70,7 @@ def parse_args(): ) try: - options, args = parser.parse_args() + options, args = parser.parse_args(args or sys.argv[1:]) mode, urls = args[0], args[1:] except (ValueError, IndexError): parser.print_usage() @@ -103,11 +103,11 @@ def __init__(self, options, urls, nrequests_per_thread): def run(self): for _i in range(self.nrequests_per_thread): try: - get(urls) + get(self.urls) except Exception as e: print(e) - if not options.continue_: + if not self.options.continue_: thread.interrupt_main() thread.exit() @@ -117,7 +117,7 @@ def run(self): URLGetterThread.counter += 1 counter = URLGetterThread.counter - should_print = options.verbose and not counter % 1000 + should_print = self.options.verbose and not counter % 1000 if should_print: print(counter)