PYTHON-5565 Add minimum version test for Encryption (#2547)

This commit is contained in:
Steven Silvester 2025-09-25 09:28:39 -05:00 committed by GitHub
parent 448a4944ff
commit fad2ccb0e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 841 additions and 692 deletions

View File

@ -151,6 +151,7 @@ functions:
- VERSION
- IS_WIN32
- REQUIRE_FIPS
- TEST_MIN_DEPS
type: test
- command: subprocess.exec
params:

File diff suppressed because it is too large Load Diff

View File

@ -179,6 +179,7 @@ buildvariants:
- name: encryption-rhel8
tasks:
- name: .test-non-standard
- name: .test-min-deps
display_name: Encryption RHEL8
run_on:
- rhel87-small
@ -209,6 +210,7 @@ buildvariants:
- name: encryption-crypt_shared-rhel8
tasks:
- name: .test-non-standard
- name: .test-min-deps
display_name: Encryption crypt_shared RHEL8
run_on:
- rhel87-small

View File

@ -6,7 +6,8 @@ SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0})
SCRIPT_DIR="$( cd -- "$SCRIPT_DIR" > /dev/null 2>&1 && pwd )"
ROOT_DIR="$(dirname $SCRIPT_DIR)"
pushd $ROOT_DIR
PREV_DIR=$(pwd)
cd $ROOT_DIR
# Try to source the env file.
if [ -f $SCRIPT_DIR/scripts/env.sh ]; then
@ -25,7 +26,17 @@ else
exit 1
fi
# Start the test runner.
uv run ${UV_ARGS} --reinstall .evergreen/scripts/run_tests.py "$@"
cleanup_tests() {
# Avoid leaving the lock file in a changed state when we change the resolution type.
if [ -n "${TEST_MIN_DEPS:-}" ]; then
git checkout uv.lock || true
fi
cd $PREV_DIR
}
popd
trap "cleanup_tests" SIGINT ERR
# Start the test runner.
uv run ${UV_ARGS} --reinstall-package pymongo .evergreen/scripts/run_tests.py "$@"
cleanup_tests

View File

@ -143,7 +143,7 @@ def create_encryption_variants() -> list[BuildVariant]:
):
expansions = get_encryption_expansions(encryption)
display_name = get_variant_name(encryption, host, **expansions)
tasks = [".test-non-standard"]
tasks = [".test-non-standard", ".test-min-deps"]
if host != "rhel8":
tasks = [".test-non-standard !.pypy"]
variant = create_variant(
@ -528,22 +528,20 @@ def create_aws_lambda_variants():
def create_server_version_tasks():
tasks = []
task_inputs = []
task_combos = set()
# All combinations of topology, auth, ssl, and sync should be tested.
for (topology, auth, ssl, sync), python in zip_cycle(
list(product(TOPOLOGIES, ["auth", "noauth"], ["ssl", "nossl"], SYNCS)), ALL_PYTHONS
):
task_inputs.append((topology, auth, ssl, sync, python))
task_combos.add((topology, auth, ssl, sync, python))
# Every python should be tested with sharded cluster, auth, ssl, with sync and async.
for python, sync in product(ALL_PYTHONS, SYNCS):
task_input = ("sharded_cluster", "auth", "ssl", sync, python)
if task_input not in task_inputs:
task_inputs.append(task_input)
task_combos.add(("sharded_cluster", "auth", "ssl", sync, python))
# Assemble the tasks.
seen = set()
for topology, auth, ssl, sync, python in task_inputs:
for topology, auth, ssl, sync, python in sorted(task_combos):
combo = f"{topology}-{auth}-{ssl}"
tags = ["server-version", f"python-{python}", combo, sync]
if combo in [
@ -558,7 +556,12 @@ def create_server_version_tasks():
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
if python not in PYPYS:
expansions["COVERAGE"] = "1"
name = get_task_name("test-server-version", python=python, sync=sync, **expansions)
name = get_task_name(
"test-server-version",
python=python,
sync=sync,
**expansions,
)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
test_vars["PYTHON_VERSION"] = python
@ -590,15 +593,15 @@ def create_no_toolchain_tasks():
def create_test_non_standard_tasks():
"""For variants that set a TEST_NAME."""
tasks = []
task_combos = []
task_combos = set()
# For each version and topology, rotate through the CPythons.
for (version, topology), python in zip_cycle(list(product(ALL_VERSIONS, TOPOLOGIES)), CPYTHONS):
pr = version == "latest"
task_combos.append((version, topology, python, pr))
task_combos.add((version, topology, python, pr))
# For each PyPy and topology, rotate through the the versions.
for (python, topology), version in zip_cycle(list(product(PYPYS, TOPOLOGIES)), ALL_VERSIONS):
task_combos.append((version, topology, python, False))
for version, topology, python, pr in task_combos:
task_combos.add((version, topology, python, False))
for version, topology, python, pr in sorted(task_combos):
auth, ssl = get_standard_auth_ssl(topology)
tags = [
"test-non-standard",
@ -621,6 +624,22 @@ def create_test_non_standard_tasks():
return tasks
def create_min_deps_tasks():
"""For variants that support testing with minimum dependencies."""
tasks = []
for topology in TOPOLOGIES:
auth, ssl = get_standard_auth_ssl(topology)
tags = ["test-min-deps", f"{topology}-{auth}-{ssl}"]
expansions = dict(AUTH=auth, SSL=ssl, TOPOLOGY=topology)
server_func = FunctionCall(func="run server", vars=expansions)
test_vars = expansions.copy()
test_vars["TEST_MIN_DEPS"] = "1"
name = get_task_name("test-min-deps", python=CPYTHONS[0], sync="sync", **test_vars)
test_func = FunctionCall(func="run tests", vars=test_vars)
tasks.append(EvgTask(name=name, tags=tags, commands=[server_func, test_func]))
return tasks
def create_standard_tasks():
"""For variants that do not set a TEST_NAME."""
tasks = []
@ -794,9 +813,12 @@ def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
tags.append("pr")
task_name = get_task_name(
f"test-ocsp-{algo}-{base_task_name}", python=python, version=version
f"test-ocsp-{algo}-{base_task_name}",
python=python,
version=version,
)
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
return tasks
@ -1075,6 +1097,7 @@ def create_run_tests_func():
"VERSION",
"IS_WIN32",
"REQUIRE_FIPS",
"TEST_MIN_DEPS",
]
args = [".evergreen/just.sh", "setup-tests", "${TEST_NAME}", "${SUB_TEST_NAME}"]
setup_cmd = get_subprocess_exec(include_expansions_in_env=includes, args=args)

View File

@ -42,6 +42,7 @@ DISPLAY_LOOKUP = dict(
sync={"sync": "Sync", "async": "Async"},
coverage={"1": "cov"},
no_ext={"1": "No C"},
test_min_deps={True: "Min Deps"},
)
HOSTS = dict()
@ -202,7 +203,7 @@ def get_common_name(base: str, sep: str, **kwargs) -> str:
name = f"Python{value}"
else:
name = f"PyPy{value.replace('pypy', '')}"
elif key.lower() in DISPLAY_LOOKUP:
elif key.lower() in DISPLAY_LOOKUP and value in DISPLAY_LOOKUP[key.lower()]:
name = DISPLAY_LOOKUP[key.lower()][value]
else:
continue

View File

@ -30,13 +30,14 @@ SUB_TEST_NAME = os.environ.get("SUB_TEST_NAME")
def list_packages():
packages = dict()
packages = set()
for distribution in importlib_metadata.distributions():
packages[distribution.name] = distribution
if distribution.name:
packages.add(distribution.name)
print("Package Version URL")
print("------------------- ----------- ----------------------------------------------------")
for name in sorted(packages):
distribution = packages[name]
distribution = importlib_metadata.distribution(name)
url = ""
if distribution.origin is not None:
url = distribution.origin.url
@ -136,6 +137,9 @@ def handle_aws_lambda() -> None:
def run() -> None:
# Add diagnostic for python version.
print("Running with python", sys.version)
# List the installed packages.
list_packages()

View File

@ -160,7 +160,6 @@ def handle_test_env() -> None:
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.
# Set an environment variable for the test name and sub test name.
write_env(f"TEST_{test_name.upper()}")
@ -178,6 +177,9 @@ def handle_test_env() -> None:
if group := GROUP_MAP.get(test_name, ""):
UV_ARGS.append(f"--group {group}")
if opts.test_min_deps:
UV_ARGS.append("--resolution=lowest-direct")
if test_name == "auth_oidc":
from oidc_tester import setup_oidc
@ -233,7 +235,7 @@ def handle_test_env() -> None:
if is_set("MONGODB_URI"):
write_env("PYMONGO_MUST_CONNECT", "true")
if is_set("DISABLE_TEST_COMMANDS") or opts.disable_test_commands:
if opts.disable_test_commands:
write_env("PYMONGO_DISABLE_TEST_COMMANDS", "1")
if test_name == "enterprise_auth":
@ -345,10 +347,10 @@ def handle_test_env() -> None:
if not (ROOT / "libmongocrypt").exists():
setup_libmongocrypt()
# TODO: Test with 'pip install pymongocrypt'
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)
if not opts.test_min_deps:
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
BASE = ROOT / "libmongocrypt/nocrypto"
@ -377,7 +379,7 @@ def handle_test_env() -> None:
if sub_test_name == "pyopenssl":
UV_ARGS.append("--extra ocsp")
if is_set("TEST_CRYPT_SHARED") or opts.crypt_shared:
if opts.crypt_shared:
config = read_env(f"{DRIVERS_TOOLS}/mo-expansion.sh")
CRYPT_SHARED_DIR = Path(config["CRYPT_SHARED_LIB_PATH"]).parent.as_posix()
LOGGER.info("Using crypt_shared_dir %s", CRYPT_SHARED_DIR)
@ -447,14 +449,14 @@ def handle_test_env() -> None:
# Add coverage if requested.
# Only cover CPython. PyPy reports suspiciously low coverage.
if (is_set("COVERAGE") or opts.cov) and platform.python_implementation() == "CPython":
if opts.cov and platform.python_implementation() == "CPython":
# Keep in sync with combine-coverage.sh.
# coverage >=5 is needed for relative_files=true.
UV_ARGS.append("--group coverage")
TEST_ARGS = f"{TEST_ARGS} --cov"
write_env("COVERAGE")
if is_set("GREEN_FRAMEWORK") or opts.green_framework:
if opts.green_framework:
framework = opts.green_framework or os.environ["GREEN_FRAMEWORK"]
UV_ARGS.append(f"--group {framework}")

View File

@ -60,6 +60,9 @@ NO_RUN_ORCHESTRATION = [
"ocsp",
]
# Mapping of env variables to options
OPTION_TO_ENV_VAR = {"cov": "COVERAGE", "crypt_shared": "TEST_CRYPT_SHARED"}
def get_test_options(
description, require_sub_test_name=True, allow_extra_opts=False
@ -94,6 +97,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(
"--test-min-deps", action="store_true", help="Test against minimum dependency versions"
)
# Add the test modifiers.
if require_sub_test_name:
@ -127,26 +133,53 @@ def get_test_options(
opts, extra_opts = parser.parse_args(), []
else:
opts, extra_opts = parser.parse_known_args()
if opts.verbose:
LOGGER.setLevel(logging.DEBUG)
elif opts.quiet:
LOGGER.setLevel(logging.WARNING)
# Convert list inputs to strings.
for name in vars(opts):
value = getattr(opts, name)
if isinstance(value, list):
setattr(opts, name, value[0])
# 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:
raise ValueError(f"Test '{test_name}' requires a sub_test_name")
if "auth" in test_name or os.environ.get("AUTH") == "auth":
handle_env_overrides(parser, opts)
if "auth" in test_name:
opts.auth = True
# 'auth_aws ecs' shouldn't have extra auth set.
if test_name == "auth_aws" and sub_test_name == "ecs":
opts.auth = False
if os.environ.get("SSL") == "ssl":
opts.ssl = True
if opts.verbose:
LOGGER.setLevel(logging.DEBUG)
elif opts.quiet:
LOGGER.setLevel(logging.WARNING)
return opts, extra_opts
def handle_env_overrides(parser: argparse.ArgumentParser, opts: argparse.Namespace) -> None:
# Get the options, and then allow environment variable overrides.
for key in vars(opts):
if key in OPTION_TO_ENV_VAR:
env_var = OPTION_TO_ENV_VAR[key]
else:
env_var = key.upper()
if env_var in os.environ:
if parser.get_default(key) != getattr(opts, key):
LOGGER.info("Overriding env var '%s' with cli option", env_var)
elif env_var == "AUTH":
opts.auth = os.environ.get("AUTH") == "auth"
elif env_var == "SSL":
ssl_opt = os.environ.get("SSL", "")
opts.ssl = ssl_opt and ssl_opt.lower() != "nossl"
elif isinstance(getattr(opts, key), bool):
if os.environ[env_var]:
setattr(opts, key, True)
else:
setattr(opts, key, os.environ[env_var])
def read_env(path: Path | str) -> dict[str, str]:
config = dict()
with Path(path).open() as fid:

View File

@ -382,6 +382,11 @@ If you are running one of the `no-responder` tests, omit the `run-server` step.
- Finally, you can use `just setup-tests --debug-log`.
- For evergreen patch builds, you can use `evergreen patch --param DEBUG_LOG=1` to enable debug logs for failed tests in the patch.
## Testing minimum dependencies
To run any of the test suites with minimum supported dependencies, pass `--test-min-deps` to
`just setup-tests`.
## Adding a new test suite
- If adding new tests files that should only be run for that test suite, add a pytest marker to the file and add

View File

@ -48,8 +48,7 @@ Tracker = "https://jira.mongodb.org/projects/PYTHON/issues"
[dependency-groups]
dev = []
pip = ["pip"]
# TODO: PYTHON-5464
gevent = ["gevent", "cffi>=2.0.0b1;python_version=='3.14'"]
gevent = ["gevent>=20.6.0"]
coverage = [
"pytest-cov",
"coverage>=5,<=7.10.6"
@ -57,7 +56,7 @@ coverage = [
mockupdb = [
"mockupdb@git+https://github.com/mongodb-labs/mongo-mockup-db@master"
]
perf = ["simplejson"]
perf = ["simplejson>=3.17.0"]
typing = [
"mypy==1.18.1",
"pyright==1.1.405",

View File

@ -1,3 +1,3 @@
pymongo-auth-aws>=1.1.0,<2.0.0
pymongocrypt>=1.13.0,<2.0.0
certifi;os.name=='nt' or sys_platform=='darwin'
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'

View File

@ -5,7 +5,7 @@
# Fallback to certifi on Windows if we can't load CA certs from the system
# store and just use certifi on macOS.
# https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_default_verify_paths
certifi;os.name=='nt' or sys_platform=='darwin'
certifi>=2023.7.22;os.name=='nt' or sys_platform=='darwin'
pyopenssl>=17.2.0
requests<3.0.0
cryptography>=2.5

View File

@ -519,6 +519,19 @@ class ClientContext:
"Libmongocrypt version must be at least %s" % str(other_version),
)
def require_pymongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import __version__ as pymongocrypt_version
version = Version.from_string(pymongocrypt_version)
return self._require(
lambda: version >= other_version,
"PyMongoCrypt version must be at least %s" % str(other_version),
)
def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(

View File

@ -519,6 +519,19 @@ class AsyncClientContext:
"Libmongocrypt version must be at least %s" % str(other_version),
)
def require_pymongocrypt_min(self, *ver):
other_version = Version(*ver)
if not _HAVE_PYMONGOCRYPT:
version = Version.from_string("0.0.0")
else:
from pymongocrypt import __version__ as pymongocrypt_version
version = Version.from_string(pymongocrypt_version)
return self._require(
lambda: version >= other_version,
"PyMongoCrypt version must be at least %s" % str(other_version),
)
def require_auth(self, func):
"""Run a test only if the server is running with auth enabled."""
return self._require(

View File

@ -3448,6 +3448,7 @@ class TestExplicitTextEncryptionProse(AsyncEncryptionIntegrationTest):
@async_client_context.require_no_standalone
@async_client_context.require_version_min(8, 2, -1)
@async_client_context.require_libmongocrypt_min(1, 15, 1)
@async_client_context.require_pymongocrypt_min(1, 16, 0)
async def asyncSetUp(self):
await super().asyncSetUp()
# Load the file key1-document.json as key1Document.

View File

@ -3430,6 +3430,7 @@ class TestExplicitTextEncryptionProse(EncryptionIntegrationTest):
@client_context.require_no_standalone
@client_context.require_version_min(8, 2, -1)
@client_context.require_libmongocrypt_min(1, 15, 1)
@client_context.require_pymongocrypt_min(1, 16, 0)
def setUp(self):
super().setUp()
# Load the file key1-document.json as key1Document.

12
uv.lock generated
View File

@ -1201,7 +1201,6 @@ coverage = [
{ name = "pytest-cov" },
]
gevent = [
{ name = "cffi", version = "2.0.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.14.*'" },
{ name = "gevent" },
]
mockupdb = [
@ -1222,8 +1221,8 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'encryption') or (sys_platform == 'darwin' and extra == 'encryption')", specifier = ">=2023.7.22" },
{ name = "certifi", marker = "(os_name == 'nt' and extra == 'ocsp') or (sys_platform == 'darwin' and extra == 'ocsp')", specifier = ">=2023.7.22" },
{ name = "cryptography", marker = "extra == 'ocsp'", specifier = ">=2.5" },
{ name = "dnspython", specifier = ">=1.16.0,<3.0.0" },
{ name = "furo", marker = "extra == 'docs'", specifier = "==2025.7.19" },
@ -1254,12 +1253,9 @@ coverage = [
{ name = "pytest-cov" },
]
dev = []
gevent = [
{ name = "cffi", marker = "python_full_version == '3.14.*'", specifier = ">=2.0.0b1" },
{ name = "gevent" },
]
gevent = [{ name = "gevent", specifier = ">=20.6.0" }]
mockupdb = [{ name = "mockupdb", git = "https://github.com/mongodb-labs/mongo-mockup-db?rev=master" }]
perf = [{ name = "simplejson" }]
perf = [{ name = "simplejson", specifier = ">=3.17.0" }]
pip = [{ name = "pip" }]
typing = [
{ name = "mypy", specifier = "==1.18.1" },