diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 72cab17dc9..d00261cc7e 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -262,7 +262,7 @@ functions: params: include_expansions_in_env: [AUTH, SSL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, COVERAGE, PYTHON_BINARY, LIBMONGOCRYPT_URL, MONGODB_URI, - DISABLE_TEST_COMMANDS, GREEN_FRAMEWORK, NO_EXT, COMPRESSORS] + DISABLE_TEST_COMMANDS, GREEN_FRAMEWORK, NO_EXT, COMPRESSORS, MONGODB_API_VERSION] binary: bash working_dir: "src" args: [.evergreen/just.sh, setup-test, "${TEST_NAME}", "${SUB_TEST_NAME}"] diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index f3c71b41ff..e1b3c779ff 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -eux +set -eu SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0}) SCRIPT_DIR="$( cd -- "$SCRIPT_DIR" > /dev/null 2>&1 && pwd )" @@ -7,10 +7,6 @@ ROOT_DIR="$(dirname $SCRIPT_DIR)" pushd $ROOT_DIR -export PIP_QUIET=1 # Quiet by default -export PIP_PREFER_BINARY=1 # Prefer binary dists by default -export UV_FROZEN=1 # Do not modify lock files - # Try to source the env file. if [ -f $SCRIPT_DIR/scripts/env.sh ]; then echo "Sourcing env inputs" @@ -25,74 +21,18 @@ if [ -f $SCRIPT_DIR/scripts/test-env.sh ]; then . $SCRIPT_DIR/scripts/test-env.sh else echo "Missing test inputs, please run 'just setup-test'" + exit 1 fi - # Source the local secrets export file if available. if [ -f "./secrets-export.sh" ]; then . "./secrets-export.sh" fi -PYTHON_IMPL=$(uv run python -c "import platform; print(platform.python_implementation())") - -# Ensure C extensions if applicable. -if [ -z "${NO_EXT:-}" ] && [ "$PYTHON_IMPL" = "CPython" ]; then - uv run --frozen tools/fail_if_no_c.py -fi - -if [ -n "${PYMONGOCRYPT_LIB:-}" ]; then - # Ensure pymongocrypt is working properly. - # shellcheck disable=SC2048 - uv run ${UV_ARGS} python -c "import pymongocrypt; print('pymongocrypt version: '+pymongocrypt.__version__)" - # shellcheck disable=SC2048 - uv run ${UV_ARGS} python -c "import pymongocrypt; print('libmongocrypt version: '+pymongocrypt.libmongocrypt_version())" - # PATH is updated by configure-env.sh for access to mongocryptd. -fi - -PYTHON_IMPL=$(uv run python -c "import platform; print(platform.python_implementation())") -echo "Running ${AUTH:-noauth} tests over ${SSL:-nossl} with python $(uv python find)" -uv run python -c 'import sys; print(sys.version)' - -# Show the installed packages -# shellcheck disable=SC2048 +# List the packages. PIP_QUIET=0 uv run ${UV_ARGS} --with pip pip list -# Record the start time for a perf test. -if [ -n "${TEST_PERF:-}" ]; then - start_time=$(date +%s) -fi - -# Run the tests, and store the results in Evergreen compatible XUnit XML -# files in the xunit-results/ directory. -TEST_ARGS=${TEST_ARGS} -if [ "$#" -ne 0 ]; then - TEST_ARGS="$*" -fi -echo "Running tests with $TEST_ARGS and uv args $UV_ARGS..." -if [ -z "${GREEN_FRAMEWORK:-}" ]; then - # shellcheck disable=SC2048 - uv run ${UV_ARGS} pytest $TEST_ARGS -else - # shellcheck disable=SC2048 - uv run ${UV_ARGS} green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS -fi -echo "Running tests with $TEST_ARGS... done." - -# Handle perf test post actions. -if [ -n "${TEST_PERF:-}" ]; then - end_time=$(date +%s) - elapsed_secs=$((end_time-start_time)) - - cat results.json - - echo "{\"failures\": 0, \"results\": [{\"status\": \"pass\", \"exit_code\": 0, \"test_file\": \"BenchMarkTests\", \"start\": $start_time, \"end\": $end_time, \"elapsed\": $elapsed_secs}]}" > report.json - - cat report.json -fi - -# Handle coverage post actions. -if [ -n "${COVERAGE:-}" ]; then - rm -rf .pytest_cache -fi +# Start the test runner. +uv run ${UV_ARGS} .evergreen/scripts/run_tests.py popd diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py new file mode 100644 index 0000000000..e41691ca81 --- /dev/null +++ b/.evergreen/scripts/run_tests.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import json +import logging +import os +import platform +import shutil +import sys +from datetime import datetime +from pathlib import Path + +import pytest + +HERE = Path(__file__).absolute().parent +ROOT = HERE.parent.parent +AUTH = os.environ.get("AUTH", "noauth") +SSL = os.environ.get("SSL", "nossl") +UV_ARGS = os.environ.get("UV_ARGS", "") +TEST_PERF = os.environ.get("TEST_PERF") +GREEN_FRAMEWORK = os.environ.get("GREEN_FRAMEWORK") +TEST_ARGS = os.environ.get("TEST_ARGS", "").split() + +LOGGER = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") + + +def handle_perf(start_time: datetime): + end_time = datetime.now() + elapsed_secs = (end_time - start_time).total_seconds() + with open("results.json") as fid: + results = json.load(fid) + LOGGER.info("results.json:\n%s", json.dumps(results, indent=2)) + + results = dict( + status="PASS", + exit_code=0, + test_file="BenchMarkTests", + start=int(start_time.timestamp()), + end=int(end_time.timestamp()), + elapsed=elapsed_secs, + ) + report = dict(failures=0, results=[results]) + LOGGER.info("report.json\n%s", json.dumps(report, indent=2)) + + with open("report.json", "w", newline="\n") as fid: + json.dump(report, fid) + + +def handle_green_framework() -> None: + if GREEN_FRAMEWORK == "eventlet": + import eventlet + + # https://github.com/eventlet/eventlet/issues/401 + eventlet.sleep() + eventlet.monkey_patch() + elif GREEN_FRAMEWORK == "gevent": + from gevent import monkey + + monkey.patch_all() + + # Never run async tests with a framework. + if len(TEST_ARGS) <= 1: + TEST_ARGS.extend(["-m", "not default_async and default"]) + else: + for i in range(len(TEST_ARGS) - 1): + if "-m" in TEST_ARGS[i]: + TEST_ARGS[i + 1] = f"not default_async and {TEST_ARGS[i + 1]}" + + LOGGER.info(f"Running tests with {GREEN_FRAMEWORK}...") + + +def handle_c_ext() -> None: + if platform.python_implementation() != "CPython": + return + sys.path.insert(0, str(ROOT / "tools")) + from fail_if_no_c import main as fail_if_no_c + + fail_if_no_c() + + +def handle_pymongocrypt() -> None: + import pymongocrypt + + LOGGER.info(f"pymongocrypt version: {pymongocrypt.__version__})") + LOGGER.info(f"libmongocrypt version: {pymongocrypt.libmongocrypt_version()})") + + +def run() -> None: + # Handle green framework first so they can patch modules. + if GREEN_FRAMEWORK: + handle_green_framework() + + # Ensure C extensions if applicable. + if not os.environ.get("NO_EXT"): + handle_c_ext() + + if os.environ.get("PYMONGOCRYPT_LIB"): + handle_pymongocrypt() + + LOGGER.info(f"Test setup:\n{AUTH=}\n{SSL=}\n{UV_ARGS=}\n{TEST_ARGS=}") + + # Record the start time for a perf test. + if TEST_PERF: + start_time = datetime.now() + + # Run the tests. + pytest.main(TEST_ARGS) + + # Handle perf test post actions. + if TEST_PERF: + handle_perf(start_time) + + # Handle coverage post actions. + if os.environ.get("COVERAGE"): + shutil.rmtree(".pytest_cache", ignore_errors=True) + + +if __name__ == "__main__": + run() diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index 96c138b4ae..78bfad7224 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -228,6 +228,9 @@ def handle_test_env() -> None: write_env("AUTH", AUTH) write_env("SSL", SSL) + write_env("PIP_QUIET") # Quiet by default. + write_env("PIP_PREFER_BINARY") # Prefer binary dists by default. + write_env("UV_FROZEN") # Do not modify lock files. # Skip CSOT tests on non-linux platforms. if PLATFORM != "linux": diff --git a/.evergreen/scripts/teardown-tests.sh b/.evergreen/scripts/teardown-tests.sh index 9c78c0965c..be1b88390f 100755 --- a/.evergreen/scripts/teardown-tests.sh +++ b/.evergreen/scripts/teardown-tests.sh @@ -24,6 +24,6 @@ if [ -n "${TEST_ENCRYPTION:-}" ]; then fi # Shut down load balancer if applicable. -if [ -n "${TEST_LOADBALANCER:-}" ]; then +if [ -n "${TEST_LOAD_BALANCER:-}" ]; then bash "${DRIVERS_TOOLS}"/.evergreen/run-load-balancer.sh stop fi diff --git a/green_framework_test.py b/green_framework_test.py deleted file mode 100644 index 037d0279c3..0000000000 --- a/green_framework_test.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2015-present MongoDB, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test PyMongo with a variety of greenlet-based monkey-patching frameworks.""" -from __future__ import annotations - -import getopt -import sys - -import pytest - - -def run_gevent(): - """Prepare to run tests with Gevent. Can raise ImportError.""" - from gevent import monkey - - monkey.patch_all() - - -def run_eventlet(): - """Prepare to run tests with Eventlet. Can raise ImportError.""" - import eventlet - - # https://github.com/eventlet/eventlet/issues/401 - eventlet.sleep() - eventlet.monkey_patch() - - -FRAMEWORKS = { - "gevent": run_gevent, - "eventlet": run_eventlet, -} - - -def list_frameworks(): - """Tell the user what framework names are valid.""" - sys.stdout.write( - """Testable frameworks: %s - -Note that membership in this list means the framework can be tested with -PyMongo, not necessarily that it is officially supported. -""" - % ", ".join(sorted(FRAMEWORKS)) - ) - - -def run(framework_name, *args): - """Run tests with monkey-patching enabled. Can raise ImportError.""" - # Monkey-patch. - FRAMEWORKS[framework_name]() - - arg_list = list(args) - - # Never run async tests with a framework - if len(arg_list) <= 1: - arg_list.extend(["-m", "not default_async and default"]) - else: - for i in range(len(arg_list) - 1): - if "-m" in arg_list[i]: - arg_list[i + 1] = f"not default_async and {arg_list[i + 1]}" - - # Run the tests. - sys.exit(pytest.main(arg_list)) - - -def main(): - """Parse options and run tests.""" - usage = f"""python {sys.argv[0]} FRAMEWORK_NAME - -Test PyMongo with a variety of greenlet-based monkey-patching frameworks. See -python {sys.argv[0]} --help-frameworks.""" - - try: - opts, args = getopt.getopt(sys.argv[1:], "h", ["help", "help-frameworks"]) - except getopt.GetoptError as err: - print(str(err)) - print(usage) - sys.exit(2) - - for option_name, _ in opts: - if option_name in ("-h", "--help"): - print(usage) - sys.exit() - elif option_name == "--help-frameworks": - list_frameworks() - sys.exit() - else: - raise AssertionError("unhandled option") - - if not args: - print(usage) - sys.exit(1) - - if args[0] not in FRAMEWORKS: - print("%r is not a testable framework.\n" % args[0]) - list_frameworks() - sys.exit(1) - - run( - args[0], - *args[1:], # Framework name. - ) # Command line args to pytest, like what test to run. - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 69249ee4c6..b86e9df6ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -234,7 +234,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?)|dummy.*)$" "RET", "ARG", "F405", "B028", "PGH001", "B018", "F403", "RUF015", "E731", "B007", "UP031", "F401", "B023", "F811"] "tools/*.py" = ["T201"] -"green_framework_test.py" = ["T201"] "hatch_build.py" = ["S"] "_setup.py" = ["SIM112"] diff --git a/test/asynchronous/helpers.py b/test/asynchronous/helpers.py index 28260d0a52..98e00e9385 100644 --- a/test/asynchronous/helpers.py +++ b/test/asynchronous/helpers.py @@ -81,7 +81,7 @@ COMPRESSORS = os.environ.get("COMPRESSORS") MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION") -TEST_LOADBALANCER = bool(os.environ.get("TEST_LOADBALANCER")) +TEST_LOADBALANCER = bool(os.environ.get("TEST_LOAD_BALANCER")) TEST_SERVERLESS = bool(os.environ.get("TEST_SERVERLESS")) SINGLE_MONGOS_LB_URI = os.environ.get("SINGLE_MONGOS_LB_URI") MULTI_MONGOS_LB_URI = os.environ.get("MULTI_MONGOS_LB_URI") diff --git a/test/helpers.py b/test/helpers.py index 3f51fde08c..627be182b5 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -81,7 +81,7 @@ COMPRESSORS = os.environ.get("COMPRESSORS") MONGODB_API_VERSION = os.environ.get("MONGODB_API_VERSION") -TEST_LOADBALANCER = bool(os.environ.get("TEST_LOADBALANCER")) +TEST_LOADBALANCER = bool(os.environ.get("TEST_LOAD_BALANCER")) TEST_SERVERLESS = bool(os.environ.get("TEST_SERVERLESS")) SINGLE_MONGOS_LB_URI = os.environ.get("SINGLE_MONGOS_LB_URI") MULTI_MONGOS_LB_URI = os.environ.get("MULTI_MONGOS_LB_URI") diff --git a/tools/fail_if_no_c.py b/tools/fail_if_no_c.py index 6848e155aa..64280a81d2 100644 --- a/tools/fail_if_no_c.py +++ b/tools/fail_if_no_c.py @@ -18,34 +18,30 @@ """ from __future__ import annotations -import os -import subprocess +import logging import sys -from pathlib import Path + +LOGGER = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") sys.path[0:0] = [""] import bson # noqa: E402 import pymongo # noqa: E402 -if not pymongo.has_c() or not bson.has_c(): - try: - from pymongo import _cmessage # type:ignore[attr-defined] # noqa: F401 - except Exception as e: - print(e) - try: - from bson import _cbson # type:ignore[attr-defined] # noqa: F401 - except Exception as e: - print(e) - sys.exit("could not load C extensions") - -if os.environ.get("ENSURE_UNIVERSAL2") == "1": - parent_dir = Path(pymongo.__path__[0]).parent - for pkg in ["pymongo", "bson", "grifs"]: - for so_file in Path(f"{parent_dir}/{pkg}").glob("*.so"): - print(f"Checking universal2 compatibility in {so_file}...") - output = subprocess.check_output(["file", so_file]) # noqa: S603, S607 - if "arm64" not in output.decode("utf-8"): - sys.exit("Universal wheel was not compiled with arm64 support") - if "x86_64" not in output.decode("utf-8"): - sys.exit("Universal wheel was not compiled with x86_64 support") + +def main() -> None: + if not pymongo.has_c() or not bson.has_c(): + try: + from pymongo import _cmessage # type:ignore[attr-defined] # noqa: F401 + except Exception as e: + LOGGER.exception(e) + try: + from bson import _cbson # type:ignore[attr-defined] # noqa: F401 + except Exception as e: + LOGGER.exception(e) + sys.exit("could not load C extensions") + + +if __name__ == "__main__": + main()