PYTHON-5149 Convert run-tests.sh to a Python script (#2155)

This commit is contained in:
Steven Silvester 2025-02-24 09:14:10 -06:00 committed by GitHub
parent 25b2d77b63
commit f27e8e123a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 150 additions and 210 deletions

View File

@ -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}"]

View File

@ -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

View File

@ -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()

View File

@ -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":

View File

@ -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

View File

@ -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()

View File

@ -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"]

View File

@ -81,7 +81,7 @@ if CA_PEM:
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")

View File

@ -81,7 +81,7 @@ if CA_PEM:
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")

View File

@ -18,34 +18,30 @@ Only really intended to be used by internal build scripts.
"""
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()