From ee18313f1651beda1809cd38451ca5c34b264f27 Mon Sep 17 00:00:00 2001 From: Iris <58442094+sleepyStick@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:37:02 -0700 Subject: [PATCH 01/14] PYTHON-4725 Async client should use tasks for SDAM instead of threads (#1896) --- pymongo/asynchronous/mongo_client.py | 6 +- pymongo/asynchronous/monitor.py | 20 +- pymongo/asynchronous/periodic_executor.py | 219 ------------------ pymongo/asynchronous/topology.py | 7 +- .../{synchronous => }/periodic_executor.py | 109 +++++++-- pymongo/synchronous/mongo_client.py | 4 +- pymongo/synchronous/monitor.py | 8 +- pymongo/synchronous/topology.py | 3 +- test/__init__.py | 9 + test/asynchronous/__init__.py | 9 + test/asynchronous/conftest.py | 2 +- test/asynchronous/test_client.py | 64 ++--- test/asynchronous/test_collection.py | 8 +- ...nnections_survive_primary_stepdown_spec.py | 8 +- test/asynchronous/test_cursor.py | 10 +- test/asynchronous/test_session.py | 1 - test/asynchronous/test_transactions.py | 6 +- test/conftest.py | 2 +- test/test_client.py | 17 +- test/test_collection.py | 5 +- ...nnections_survive_primary_stepdown_spec.py | 8 +- test/test_monitor.py | 2 +- test/utils.py | 11 +- tools/synchro.py | 4 + 24 files changed, 228 insertions(+), 314 deletions(-) delete mode 100644 pymongo/asynchronous/periodic_executor.py rename pymongo/{synchronous => }/periodic_executor.py (69%) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index bfae302dac..d2b45fd64a 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -59,8 +59,8 @@ from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry from bson.timestamp import Timestamp -from pymongo import _csot, common, helpers_shared, uri_parser -from pymongo.asynchronous import client_session, database, periodic_executor +from pymongo import _csot, common, helpers_shared, periodic_executor, uri_parser +from pymongo.asynchronous import client_session, database from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream from pymongo.asynchronous.client_bulk import _AsyncClientBulk from pymongo.asynchronous.client_session import _EmptyServerSession @@ -908,7 +908,7 @@ async def target() -> bool: await AsyncMongoClient._process_periodic_tasks(client) return True - executor = periodic_executor.PeriodicExecutor( + executor = periodic_executor.AsyncPeriodicExecutor( interval=common.KILL_CURSOR_FREQUENCY, min_interval=common.MIN_HEARTBEAT_INTERVAL, target=target, diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index f9e912b084..bbfd6a2998 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -22,14 +22,13 @@ import weakref from typing import TYPE_CHECKING, Any, Mapping, Optional, cast -from pymongo import common +from pymongo import common, periodic_executor from pymongo._csot import MovingMinimum -from pymongo.asynchronous import periodic_executor -from pymongo.asynchronous.periodic_executor import _shutdown_executors from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled from pymongo.hello import Hello from pymongo.lock import _create_lock from pymongo.logger import _SDAM_LOGGER, _debug_log, _SDAMStatusMessage +from pymongo.periodic_executor import _shutdown_executors from pymongo.pool_options import _is_faas from pymongo.read_preferences import MovingAverage from pymongo.server_description import ServerDescription @@ -76,7 +75,7 @@ async def target() -> bool: await monitor._run() # type:ignore[attr-defined] return True - executor = periodic_executor.PeriodicExecutor( + executor = periodic_executor.AsyncPeriodicExecutor( interval=interval, min_interval=min_interval, target=target, name=name ) @@ -112,9 +111,9 @@ async def close(self) -> None: """ self.gc_safe_close() - def join(self, timeout: Optional[int] = None) -> None: + async def join(self, timeout: Optional[int] = None) -> None: """Wait for the monitor to stop.""" - self._executor.join(timeout) + await self._executor.join(timeout) def request_check(self) -> None: """If the monitor is sleeping, wake it soon.""" @@ -139,7 +138,7 @@ def __init__( """ super().__init__( topology, - "pymongo_server_monitor_thread", + "pymongo_server_monitor_task", topology_settings.heartbeat_frequency, common.MIN_HEARTBEAT_INTERVAL, ) @@ -250,7 +249,7 @@ async def _check_server(self) -> ServerDescription: except (OperationFailure, NotPrimaryError) as exc: # Update max cluster time even when hello fails. details = cast(Mapping[str, Any], exc.details) - self._topology.receive_cluster_time(details.get("$clusterTime")) + await self._topology.receive_cluster_time(details.get("$clusterTime")) raise except ReferenceError: raise @@ -434,7 +433,7 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool """ super().__init__( topology, - "pymongo_server_rtt_thread", + "pymongo_server_rtt_task", topology_settings.heartbeat_frequency, common.MIN_HEARTBEAT_INTERVAL, ) @@ -531,4 +530,5 @@ def _shutdown_resources() -> None: shutdown() -atexit.register(_shutdown_resources) +if _IS_SYNC: + atexit.register(_shutdown_resources) diff --git a/pymongo/asynchronous/periodic_executor.py b/pymongo/asynchronous/periodic_executor.py deleted file mode 100644 index f3d2fddba3..0000000000 --- a/pymongo/asynchronous/periodic_executor.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2014-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. - -"""Run a target function on a background thread.""" - -from __future__ import annotations - -import asyncio -import sys -import threading -import time -import weakref -from typing import Any, Optional - -from pymongo.lock import _ALock, _create_lock - -_IS_SYNC = False - - -class PeriodicExecutor: - def __init__( - self, - interval: float, - min_interval: float, - target: Any, - name: Optional[str] = None, - ): - """Run a target function periodically on a background thread. - - If the target's return value is false, the executor stops. - - :param interval: Seconds between calls to `target`. - :param min_interval: Minimum seconds between calls if `wake` is - called very often. - :param target: A function. - :param name: A name to give the underlying thread. - """ - # threading.Event and its internal condition variable are expensive - # in Python 2, see PYTHON-983. Use a boolean to know when to wake. - # The executor's design is constrained by several Python issues, see - # "periodic_executor.rst" in this repository. - self._event = False - self._interval = interval - self._min_interval = min_interval - self._target = target - self._stopped = False - self._thread: Optional[threading.Thread] = None - self._name = name - self._skip_sleep = False - self._thread_will_exit = False - self._lock = _ALock(_create_lock()) - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}(name={self._name}) object at 0x{id(self):x}>" - - def _run_async(self) -> None: - # The default asyncio loop implementation on Windows - # has issues with sharing sockets across loops (https://github.com/python/cpython/issues/122240) - # We explicitly use a different loop implementation here to prevent that issue - if sys.platform == "win32": - loop = asyncio.SelectorEventLoop() - try: - loop.run_until_complete(self._run()) # type: ignore[func-returns-value] - finally: - loop.close() - else: - asyncio.run(self._run()) # type: ignore[func-returns-value] - - def open(self) -> None: - """Start. Multiple calls have no effect. - - Not safe to call from multiple threads at once. - """ - with self._lock: - if self._thread_will_exit: - # If the background thread has read self._stopped as True - # there is a chance that it has not yet exited. The call to - # join should not block indefinitely because there is no - # other work done outside the while loop in self._run. - try: - assert self._thread is not None - self._thread.join() - except ReferenceError: - # Thread terminated. - pass - self._thread_will_exit = False - self._stopped = False - started: Any = False - try: - started = self._thread and self._thread.is_alive() - except ReferenceError: - # Thread terminated. - pass - - if not started: - if _IS_SYNC: - thread = threading.Thread(target=self._run, name=self._name) - else: - thread = threading.Thread(target=self._run_async, name=self._name) - thread.daemon = True - self._thread = weakref.proxy(thread) - _register_executor(self) - # Mitigation to RuntimeError firing when thread starts on shutdown - # https://github.com/python/cpython/issues/114570 - try: - thread.start() - except RuntimeError as e: - if "interpreter shutdown" in str(e) or sys.is_finalizing(): - self._thread = None - return - raise - - def close(self, dummy: Any = None) -> None: - """Stop. To restart, call open(). - - The dummy parameter allows an executor's close method to be a weakref - callback; see monitor.py. - """ - self._stopped = True - - def join(self, timeout: Optional[int] = None) -> None: - if self._thread is not None: - try: - self._thread.join(timeout) - except (ReferenceError, RuntimeError): - # Thread already terminated, or not yet started. - pass - - def wake(self) -> None: - """Execute the target function soon.""" - self._event = True - - def update_interval(self, new_interval: int) -> None: - self._interval = new_interval - - def skip_sleep(self) -> None: - self._skip_sleep = True - - async def _should_stop(self) -> bool: - async with self._lock: - if self._stopped: - self._thread_will_exit = True - return True - return False - - async def _run(self) -> None: - while not await self._should_stop(): - try: - if not await self._target(): - self._stopped = True - break - except BaseException: - async with self._lock: - self._stopped = True - self._thread_will_exit = True - - raise - - if self._skip_sleep: - self._skip_sleep = False - else: - deadline = time.monotonic() + self._interval - while not self._stopped and time.monotonic() < deadline: - await asyncio.sleep(self._min_interval) - if self._event: - break # Early wake. - - self._event = False - - -# _EXECUTORS has a weakref to each running PeriodicExecutor. Once started, -# an executor is kept alive by a strong reference from its thread and perhaps -# from other objects. When the thread dies and all other referrers are freed, -# the executor is freed and removed from _EXECUTORS. If any threads are -# running when the interpreter begins to shut down, we try to halt and join -# them to avoid spurious errors. -_EXECUTORS = set() - - -def _register_executor(executor: PeriodicExecutor) -> None: - ref = weakref.ref(executor, _on_executor_deleted) - _EXECUTORS.add(ref) - - -def _on_executor_deleted(ref: weakref.ReferenceType[PeriodicExecutor]) -> None: - _EXECUTORS.remove(ref) - - -def _shutdown_executors() -> None: - if _EXECUTORS is None: - return - - # Copy the set. Stopping threads has the side effect of removing executors. - executors = list(_EXECUTORS) - - # First signal all executors to close... - for ref in executors: - executor = ref() - if executor: - executor.close() - - # ...then try to join them. - for ref in executors: - executor = ref() - if executor: - executor.join(1) - - executor = None diff --git a/pymongo/asynchronous/topology.py b/pymongo/asynchronous/topology.py index 82af4257ba..f0cb56cbf1 100644 --- a/pymongo/asynchronous/topology.py +++ b/pymongo/asynchronous/topology.py @@ -27,8 +27,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, cast -from pymongo import _csot, common, helpers_shared -from pymongo.asynchronous import periodic_executor +from pymongo import _csot, common, helpers_shared, periodic_executor from pymongo.asynchronous.client_session import _ServerSession, _ServerSessionPool from pymongo.asynchronous.monitor import SrvMonitor from pymongo.asynchronous.pool import Pool @@ -185,7 +184,7 @@ def __init__(self, topology_settings: TopologySettings): async def target() -> bool: return process_events_queue(weak) - executor = periodic_executor.PeriodicExecutor( + executor = periodic_executor.AsyncPeriodicExecutor( interval=common.EVENTS_QUEUE_FREQUENCY, min_interval=common.MIN_HEARTBEAT_INTERVAL, target=target, @@ -742,7 +741,7 @@ async def close(self) -> None: if self._publish_server or self._publish_tp: # Make sure the events executor thread is fully closed before publishing the remaining events self.__events_executor.close() - self.__events_executor.join(1) + await self.__events_executor.join(1) process_events_queue(weakref.ref(self._events)) # type: ignore[arg-type] @property diff --git a/pymongo/synchronous/periodic_executor.py b/pymongo/periodic_executor.py similarity index 69% rename from pymongo/synchronous/periodic_executor.py rename to pymongo/periodic_executor.py index 525268b14b..216a4457c7 100644 --- a/pymongo/synchronous/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -25,7 +25,96 @@ from pymongo.lock import _create_lock -_IS_SYNC = True +_IS_SYNC = False + + +class AsyncPeriodicExecutor: + def __init__( + self, + interval: float, + min_interval: float, + target: Any, + name: Optional[str] = None, + ): + """Run a target function periodically on a background task. + + If the target's return value is false, the executor stops. + + :param interval: Seconds between calls to `target`. + :param min_interval: Minimum seconds between calls if `wake` is + called very often. + :param target: A function. + :param name: A name to give the underlying task. + """ + self._event = False + self._interval = interval + self._min_interval = min_interval + self._target = target + self._stopped = False + self._task: Optional[asyncio.Task] = None + self._name = name + self._skip_sleep = False + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(name={self._name}) object at 0x{id(self):x}>" + + def open(self) -> None: + """Start. Multiple calls have no effect.""" + self._stopped = False + started = self._task and not self._task.done() + + if not started: + self._task = asyncio.get_event_loop().create_task(self._run(), name=self._name) + + def close(self, dummy: Any = None) -> None: + """Stop. To restart, call open(). + + The dummy parameter allows an executor's close method to be a weakref + callback; see monitor.py. + """ + self._stopped = True + + async def join(self, timeout: Optional[int] = None) -> None: + if self._task is not None: + try: + await asyncio.wait_for(self._task, timeout=timeout) # type-ignore: [arg-type] + except asyncio.TimeoutError: + # Task timed out + pass + except asyncio.exceptions.CancelledError: + # Task was already finished, or not yet started. + pass + + def wake(self) -> None: + """Execute the target function soon.""" + self._event = True + + def update_interval(self, new_interval: int) -> None: + self._interval = new_interval + + def skip_sleep(self) -> None: + self._skip_sleep = True + + async def _run(self) -> None: + while not self._stopped: + try: + if not await self._target(): + self._stopped = True + break + except BaseException: + self._stopped = True + raise + + if self._skip_sleep: + self._skip_sleep = False + else: + deadline = time.monotonic() + self._interval + while not self._stopped and time.monotonic() < deadline: + await asyncio.sleep(self._min_interval) + if self._event: + break # Early wake. + + self._event = False class PeriodicExecutor: @@ -64,19 +153,6 @@ def __init__( def __repr__(self) -> str: return f"<{self.__class__.__name__}(name={self._name}) object at 0x{id(self):x}>" - def _run_async(self) -> None: - # The default asyncio loop implementation on Windows - # has issues with sharing sockets across loops (https://github.com/python/cpython/issues/122240) - # We explicitly use a different loop implementation here to prevent that issue - if sys.platform == "win32": - loop = asyncio.SelectorEventLoop() - try: - loop.run_until_complete(self._run()) # type: ignore[func-returns-value] - finally: - loop.close() - else: - asyncio.run(self._run()) # type: ignore[func-returns-value] - def open(self) -> None: """Start. Multiple calls have no effect. @@ -104,10 +180,7 @@ def open(self) -> None: pass if not started: - if _IS_SYNC: - thread = threading.Thread(target=self._run, name=self._name) - else: - thread = threading.Thread(target=self._run_async, name=self._name) + thread = threading.Thread(target=self._run, name=self._name) thread.daemon = True self._thread = weakref.proxy(thread) _register_executor(self) diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 1351cb200f..8f4d9cacf2 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -58,7 +58,7 @@ from bson.codec_options import DEFAULT_CODEC_OPTIONS, CodecOptions, TypeRegistry from bson.timestamp import Timestamp -from pymongo import _csot, common, helpers_shared, uri_parser +from pymongo import _csot, common, helpers_shared, periodic_executor, uri_parser from pymongo.client_options import ClientOptions from pymongo.errors import ( AutoReconnect, @@ -91,7 +91,7 @@ from pymongo.results import ClientBulkWriteResult from pymongo.server_selectors import writable_server_selector from pymongo.server_type import SERVER_TYPE -from pymongo.synchronous import client_session, database, periodic_executor +from pymongo.synchronous import client_session, database from pymongo.synchronous.change_stream import ChangeStream, ClusterChangeStream from pymongo.synchronous.client_bulk import _ClientBulk from pymongo.synchronous.client_session import _EmptyServerSession diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index 3f9bb2ea75..a806670f2c 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -22,18 +22,17 @@ import weakref from typing import TYPE_CHECKING, Any, Mapping, Optional, cast -from pymongo import common +from pymongo import common, periodic_executor from pymongo._csot import MovingMinimum from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled from pymongo.hello import Hello from pymongo.lock import _create_lock from pymongo.logger import _SDAM_LOGGER, _debug_log, _SDAMStatusMessage +from pymongo.periodic_executor import _shutdown_executors from pymongo.pool_options import _is_faas from pymongo.read_preferences import MovingAverage from pymongo.server_description import ServerDescription from pymongo.srv_resolver import _SrvResolver -from pymongo.synchronous import periodic_executor -from pymongo.synchronous.periodic_executor import _shutdown_executors if TYPE_CHECKING: from pymongo.synchronous.pool import Connection, Pool, _CancellationContext @@ -531,4 +530,5 @@ def _shutdown_resources() -> None: shutdown() -atexit.register(_shutdown_resources) +if _IS_SYNC: + atexit.register(_shutdown_resources) diff --git a/pymongo/synchronous/topology.py b/pymongo/synchronous/topology.py index a350c1702e..e34de6bc50 100644 --- a/pymongo/synchronous/topology.py +++ b/pymongo/synchronous/topology.py @@ -27,7 +27,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, cast -from pymongo import _csot, common, helpers_shared +from pymongo import _csot, common, helpers_shared, periodic_executor from pymongo.errors import ( ConnectionFailure, InvalidOperation, @@ -56,7 +56,6 @@ secondary_server_selector, writable_server_selector, ) -from pymongo.synchronous import periodic_executor from pymongo.synchronous.client_session import _ServerSession, _ServerSessionPool from pymongo.synchronous.monitor import SrvMonitor from pymongo.synchronous.pool import Pool diff --git a/test/__init__.py b/test/__init__.py index af12bc032a..6be3b49ce6 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -859,6 +859,15 @@ def max_message_size_bytes(self): client_context = ClientContext() +def reset_client_context(): + if _IS_SYNC: + # sync tests don't need to reset a client context + return + client_context.client.close() + client_context.client = None + client_context._init_client() + + class PyMongoTestCase(unittest.TestCase): def assertEqualCommand(self, expected, actual, msg=None): self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 2a44785b2f..1a386fe766 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -861,6 +861,15 @@ async def max_message_size_bytes(self): async_client_context = AsyncClientContext() +async def reset_client_context(): + if _IS_SYNC: + # sync tests don't need to reset a client context + return + await async_client_context.client.close() + async_client_context.client = None + await async_client_context._init_client() + + class AsyncPyMongoTestCase(unittest.IsolatedAsyncioTestCase): def assertEqualCommand(self, expected, actual, msg=None): self.assertEqual(sanitize_cmd(expected), sanitize_cmd(actual), msg) diff --git a/test/asynchronous/conftest.py b/test/asynchronous/conftest.py index e443dff6c0..a27a9f213d 100644 --- a/test/asynchronous/conftest.py +++ b/test/asynchronous/conftest.py @@ -22,7 +22,7 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() -@pytest_asyncio.fixture(scope="session", autouse=True) +@pytest_asyncio.fixture(scope="package", autouse=True) async def test_setup_and_teardown(): await async_setup() yield diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index faa23348c9..c4d71cdbe6 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -73,7 +73,6 @@ is_greenthread_patched, lazy_client_trial, one, - wait_until, ) import bson @@ -693,8 +692,8 @@ async def test_max_idle_time_reaper_removes_stale_minPoolSize(self): # When the reaper runs at the same time as the get_socket, two # connections could be created and checked into the pool. self.assertGreaterEqual(len(server._pool.conns), 1) - wait_until(lambda: conn not in server._pool.conns, "remove stale socket") - wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") + await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") + await async_wait_until(lambda: len(server._pool.conns) >= 1, "replace stale socket") async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self): with client_knobs(kill_cursor_frequency=0.1): @@ -710,8 +709,8 @@ async def test_max_idle_time_reaper_does_not_exceed_maxPoolSize(self): # When the reaper runs at the same time as the get_socket, # maxPoolSize=1 should prevent two connections from being created. self.assertEqual(1, len(server._pool.conns)) - wait_until(lambda: conn not in server._pool.conns, "remove stale socket") - wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") + await async_wait_until(lambda: conn not in server._pool.conns, "remove stale socket") + await async_wait_until(lambda: len(server._pool.conns) == 1, "replace stale socket") async def test_max_idle_time_reaper_removes_stale(self): with client_knobs(kill_cursor_frequency=0.1): @@ -727,7 +726,7 @@ async def test_max_idle_time_reaper_removes_stale(self): async with server._pool.checkout() as conn_two: pass self.assertIs(conn_one, conn_two) - wait_until( + await async_wait_until( lambda: len(server._pool.conns) == 0, "stale socket reaped and new one NOT added to the pool", ) @@ -745,7 +744,7 @@ async def test_min_pool_size(self): server = await (await client._get_topology()).select_server( readable_server_selector, _Op.TEST ) - wait_until( + await async_wait_until( lambda: len(server._pool.conns) == 10, "pool initialized with 10 connections", ) @@ -753,7 +752,7 @@ async def test_min_pool_size(self): # Assert that if a socket is closed, a new one takes its place async with server._pool.checkout() as conn: conn.close_conn(None) - wait_until( + await async_wait_until( lambda: len(server._pool.conns) == 10, "a closed socket gets replaced from the pool", ) @@ -941,8 +940,10 @@ async def test_repr(self): async with eval(the_repr) as client_two: self.assertEqual(client_two, client) - def test_getters(self): - wait_until(lambda: async_client_context.nodes == self.client.nodes, "find all nodes") + async def test_getters(self): + await async_wait_until( + lambda: async_client_context.nodes == self.client.nodes, "find all nodes" + ) async def test_list_databases(self): cmd_docs = (await self.client.admin.command("listDatabases"))["databases"] @@ -1067,14 +1068,21 @@ async def test_uri_connect_option(self): self.assertFalse(client._topology._opened) # Ensure kill cursors thread has not been started. - kc_thread = client._kill_cursors_executor._thread - self.assertFalse(kc_thread and kc_thread.is_alive()) - + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + self.assertFalse(kc_thread and kc_thread.is_alive()) + else: + kc_task = client._kill_cursors_executor._task + self.assertFalse(kc_task and not kc_task.done()) # Using the client should open topology and start the thread. await client.admin.command("ping") self.assertTrue(client._topology._opened) - kc_thread = client._kill_cursors_executor._thread - self.assertTrue(kc_thread and kc_thread.is_alive()) + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + self.assertTrue(kc_thread and kc_thread.is_alive()) + else: + kc_task = client._kill_cursors_executor._task + self.assertTrue(kc_task and not kc_task.done()) async def test_close_does_not_open_servers(self): client = await self.async_rs_client(connect=False) @@ -1610,7 +1618,7 @@ def init(self, *args): await async_client_context.port, ) await self.async_single_client(uri, event_listeners=[listener]) - wait_until( + await async_wait_until( lambda: len(listener.results) >= 2, "record two ServerHeartbeatStartedEvents" ) @@ -1768,16 +1776,16 @@ async def test_background_connections_do_not_hold_locks(self): pool = await async_get_pool(client) original_connect = pool.connect - def stall_connect(*args, **kwargs): - time.sleep(2) - return original_connect(*args, **kwargs) + async def stall_connect(*args, **kwargs): + await asyncio.sleep(2) + return await original_connect(*args, **kwargs) pool.connect = stall_connect # Un-patch Pool.connect to break the cyclic reference. self.addCleanup(delattr, pool, "connect") # Wait for the background thread to start creating connections - wait_until(lambda: len(pool.conns) > 1, "start creating connections") + await async_wait_until(lambda: len(pool.conns) > 1, "start creating connections") # Assert that application operations do not block. for _ in range(10): @@ -1860,7 +1868,7 @@ async def test_process_periodic_tasks(self): await client.close() # Add cursor to kill cursors queue del cursor - wait_until( + await async_wait_until( lambda: client._kill_cursors_queue, "waited for cursor to be added to queue", ) @@ -2218,7 +2226,7 @@ async def test_exhaust_getmore_network_error(self): await cursor.to_list() self.assertTrue(conn.closed) - wait_until( + await async_wait_until( lambda: len(client._kill_cursors_queue) == 0, "waited for all killCursor requests to complete", ) @@ -2389,7 +2397,7 @@ async def test_discover_primary(self): ) self.addAsyncCleanup(c.close) - wait_until(lambda: len(c.nodes) == 3, "connect") + await async_wait_until(lambda: len(c.nodes) == 3, "connect") self.assertEqual(await c.address, ("a", 1)) # Fail over. @@ -2416,7 +2424,7 @@ async def test_reconnect(self): ) self.addAsyncCleanup(c.close) - wait_until(lambda: len(c.nodes) == 3, "connect") + await async_wait_until(lambda: len(c.nodes) == 3, "connect") # Total failure. c.kill_host("a:1") @@ -2458,7 +2466,7 @@ async def _test_network_error(self, operation_callback): c.set_wire_version_range("a:1", 2, MIN_SUPPORTED_WIRE_VERSION) c.set_wire_version_range("b:2", 2, MIN_SUPPORTED_WIRE_VERSION + 1) await (await c._get_topology()).select_servers(writable_server_selector, _Op.TEST) - wait_until(lambda: len(c.nodes) == 2, "connect") + await async_wait_until(lambda: len(c.nodes) == 2, "connect") c.kill_host("a:1") @@ -2530,11 +2538,11 @@ async def test_rs_client_does_not_maintain_pool_to_arbiters(self): ) self.addAsyncCleanup(c.close) - wait_until(lambda: len(c.nodes) == 3, "connect") + await async_wait_until(lambda: len(c.nodes) == 3, "connect") self.assertEqual(await c.address, ("a", 1)) self.assertEqual(await c.arbiters, {("c", 3)}) # Assert that we create 2 and only 2 pooled connections. - listener.wait_for_event(monitoring.ConnectionReadyEvent, 2) + await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 2) self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 2) # Assert that we do not create connections to arbiters. arbiter = c._topology.get_server_by_address(("c", 3)) @@ -2560,7 +2568,7 @@ async def test_direct_client_maintains_pool_to_arbiter(self): ) self.addAsyncCleanup(c.close) - wait_until(lambda: len(c.nodes) == 1, "connect") + await async_wait_until(lambda: len(c.nodes) == 1, "connect") self.assertEqual(await c.address, ("c", 3)) # Assert that we create 1 pooled connection. listener.wait_for_event(monitoring.ConnectionReadyEvent, 1) diff --git a/test/asynchronous/test_collection.py b/test/asynchronous/test_collection.py index 612090b69f..470425f4ce 100644 --- a/test/asynchronous/test_collection.py +++ b/test/asynchronous/test_collection.py @@ -39,7 +39,6 @@ async_get_pool, async_is_mongos, async_wait_until, - wait_until, ) from bson import encode @@ -1022,7 +1021,10 @@ async def test_replace_bypass_document_validation(self): await db.test.insert_one({"y": 1}, bypass_document_validation=True) await db_w0.test.replace_one({"y": 1}, {"x": 1}, bypass_document_validation=True) - await async_wait_until(lambda: db_w0.test.find_one({"x": 1}), "find w:0 replaced document") + async def predicate(): + return await db_w0.test.find_one({"x": 1}) + + await async_wait_until(predicate, "find w:0 replaced document") async def test_update_bypass_document_validation(self): db = self.db @@ -1870,7 +1872,7 @@ async def test_exhaust(self): await cur.close() cur = None # Wait until the background thread returns the socket. - wait_until(lambda: pool.active_sockets == 0, "return socket") + await async_wait_until(lambda: pool.active_sockets == 0, "return socket") # The socket should be discarded. self.assertEqual(0, len(pool.conns)) diff --git a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py index 289cf49751..dc04cb28a7 100644 --- a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py +++ b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py @@ -19,7 +19,12 @@ sys.path[0:0] = [""] -from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest +from test.asynchronous import ( + AsyncIntegrationTest, + async_client_context, + reset_client_context, + unittest, +) from test.asynchronous.helpers import async_repl_set_step_down from test.utils import ( CMAPListener, @@ -60,6 +65,7 @@ async def _setup_class(cls): @classmethod async def _tearDown_class(cls): await cls.client.close() + await reset_client_context() async def asyncSetUp(self): # Note that all ops use same write-concern as self.db (majority). diff --git a/test/asynchronous/test_cursor.py b/test/asynchronous/test_cursor.py index e79ad00641..f7b795cdae 100644 --- a/test/asynchronous/test_cursor.py +++ b/test/asynchronous/test_cursor.py @@ -34,9 +34,9 @@ AllowListEventListener, EventListener, OvertCommandListener, + async_wait_until, delay, ignore_deprecations, - wait_until, ) from bson import decode_all @@ -1324,8 +1324,8 @@ async def test_timeout_kills_cursor_asynchronously(self): with self.assertRaises(ExecutionTimeout): await cursor.next() - def assertCursorKilled(): - wait_until( + async def assertCursorKilled(): + await async_wait_until( lambda: len(listener.succeeded_events), "find successful killCursors command", ) @@ -1335,7 +1335,7 @@ def assertCursorKilled(): self.assertEqual(1, len(listener.succeeded_events)) self.assertEqual("killCursors", listener.succeeded_events[0].command_name) - assertCursorKilled() + await assertCursorKilled() listener.reset() cursor = await coll.aggregate([], batchSize=1) @@ -1345,7 +1345,7 @@ def assertCursorKilled(): with self.assertRaises(ExecutionTimeout): await cursor.next() - assertCursorKilled() + await assertCursorKilled() def test_delete_not_initialized(self): # Creating a cursor with invalid arguments will not run __init__ diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index d264b5ecb0..c1dac6f56d 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -37,7 +37,6 @@ EventListener, ExceptionCatchingThread, async_wait_until, - wait_until, ) from bson import DBRef diff --git a/test/asynchronous/test_transactions.py b/test/asynchronous/test_transactions.py index b5d0686417..229046e79b 100644 --- a/test/asynchronous/test_transactions.py +++ b/test/asynchronous/test_transactions.py @@ -26,7 +26,7 @@ from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest from test.utils import ( OvertCommandListener, - wait_until, + async_wait_until, ) from typing import List @@ -162,7 +162,7 @@ async def test_unpin_for_next_transaction(self): client = await self.async_rs_client( async_client_context.mongos_seeds(), localThresholdMS=1000 ) - wait_until(lambda: len(client.nodes) > 1, "discover both mongoses") + await async_wait_until(lambda: len(client.nodes) > 1, "discover both mongoses") coll = client.test.test # Create the collection. await coll.insert_one({}) @@ -191,7 +191,7 @@ async def test_unpin_for_non_transaction_operation(self): client = await self.async_rs_client( async_client_context.mongos_seeds(), localThresholdMS=1000 ) - wait_until(lambda: len(client.nodes) > 1, "discover both mongoses") + await async_wait_until(lambda: len(client.nodes) > 1, "discover both mongoses") coll = client.test.test # Create the collection. await coll.insert_one({}) diff --git a/test/conftest.py b/test/conftest.py index a3d954c7c3..91fad28d0a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -20,7 +20,7 @@ def event_loop_policy(): return asyncio.get_event_loop_policy() -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="package", autouse=True) def test_setup_and_teardown(): setup() yield diff --git a/test/test_client.py b/test/test_client.py index be1994dd93..a4c521157b 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -1041,14 +1041,21 @@ def test_uri_connect_option(self): self.assertFalse(client._topology._opened) # Ensure kill cursors thread has not been started. - kc_thread = client._kill_cursors_executor._thread - self.assertFalse(kc_thread and kc_thread.is_alive()) - + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + self.assertFalse(kc_thread and kc_thread.is_alive()) + else: + kc_task = client._kill_cursors_executor._task + self.assertFalse(kc_task and not kc_task.done()) # Using the client should open topology and start the thread. client.admin.command("ping") self.assertTrue(client._topology._opened) - kc_thread = client._kill_cursors_executor._thread - self.assertTrue(kc_thread and kc_thread.is_alive()) + if _IS_SYNC: + kc_thread = client._kill_cursors_executor._thread + self.assertTrue(kc_thread and kc_thread.is_alive()) + else: + kc_task = client._kill_cursors_executor._task + self.assertTrue(kc_task and not kc_task.done()) def test_close_does_not_open_servers(self): client = self.rs_client(connect=False) diff --git a/test/test_collection.py b/test/test_collection.py index a2c3b0b0b6..f2f01ac686 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -1009,7 +1009,10 @@ def test_replace_bypass_document_validation(self): db.test.insert_one({"y": 1}, bypass_document_validation=True) db_w0.test.replace_one({"y": 1}, {"x": 1}, bypass_document_validation=True) - wait_until(lambda: db_w0.test.find_one({"x": 1}), "find w:0 replaced document") + def predicate(): + return db_w0.test.find_one({"x": 1}) + + wait_until(predicate, "find w:0 replaced document") def test_update_bypass_document_validation(self): db = self.db diff --git a/test/test_connections_survive_primary_stepdown_spec.py b/test/test_connections_survive_primary_stepdown_spec.py index 54cc4e0482..984d700fb3 100644 --- a/test/test_connections_survive_primary_stepdown_spec.py +++ b/test/test_connections_survive_primary_stepdown_spec.py @@ -19,7 +19,12 @@ sys.path[0:0] = [""] -from test import IntegrationTest, client_context, unittest +from test import ( + IntegrationTest, + client_context, + reset_client_context, + unittest, +) from test.helpers import repl_set_step_down from test.utils import ( CMAPListener, @@ -60,6 +65,7 @@ def _setup_class(cls): @classmethod def _tearDown_class(cls): cls.client.close() + reset_client_context() def setUp(self): # Note that all ops use same write-concern as self.db (majority). diff --git a/test/test_monitor.py b/test/test_monitor.py index f8e9443fae..a704f3d8cb 100644 --- a/test/test_monitor.py +++ b/test/test_monitor.py @@ -29,7 +29,7 @@ wait_until, ) -from pymongo.synchronous.periodic_executor import _EXECUTORS +from pymongo.periodic_executor import _EXECUTORS def unregistered(ref): diff --git a/test/utils.py b/test/utils.py index 9c78cff3ad..174b1708ba 100644 --- a/test/utils.py +++ b/test/utils.py @@ -98,6 +98,12 @@ def wait_for_event(self, event, count): """Wait for a number of events to be published, or fail.""" wait_until(lambda: self.event_count(event) >= count, f"find {count} {event} event(s)") + async def async_wait_for_event(self, event, count): + """Wait for a number of events to be published, or fail.""" + await async_wait_until( + lambda: self.event_count(event) >= count, f"find {count} {event} event(s)" + ) + class CMAPListener(BaseListener, monitoring.ConnectionPoolListener): def connection_created(self, event): @@ -789,7 +795,10 @@ async def async_wait_until(predicate, success_description, timeout=10): start = time.time() interval = min(float(timeout) / 100, 0.1) while True: - retval = await predicate() + if iscoroutinefunction(predicate): + retval = await predicate() + else: + retval = predicate() if retval: return retval diff --git a/tools/synchro.py b/tools/synchro.py index 0ec8985a05..c3c0b568ed 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -108,6 +108,10 @@ "async_set_fail_point": "set_fail_point", "async_ensure_all_connected": "ensure_all_connected", "async_repl_set_step_down": "repl_set_step_down", + "AsyncPeriodicExecutor": "PeriodicExecutor", + "async_wait_for_event": "wait_for_event", + "pymongo_server_monitor_task": "pymongo_server_monitor_thread", + "pymongo_server_rtt_task": "pymongo_server_rtt_thread", } docstring_replacements: dict[tuple[str, str], str] = { From 3e5890ae9966e54ac918e9c9ccb756a4480b14fc Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 15 Oct 2024 14:54:40 -0400 Subject: [PATCH 02/14] Resync with master (#1932) Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com> Co-authored-by: Steven Silvester Co-authored-by: Shane Harvey --- .evergreen/config.yml | 174 +- .evergreen/hatch.sh | 18 +- .evergreen/run-tests.sh | 4 +- .evergreen/scripts/generate_config.py | 167 ++ pymongo/asynchronous/encryption.py | 12 +- pymongo/asynchronous/mongo_client.py | 7 - pymongo/network_layer.py | 27 +- pymongo/synchronous/encryption.py | 12 +- pymongo/synchronous/mongo_client.py | 7 - test/__init__.py | 11 +- test/asynchronous/__init__.py | 11 +- test/asynchronous/test_client.py | 18 +- test/asynchronous/test_command_logging.py | 44 + test/asynchronous/test_command_monitoring.py | 45 + test/asynchronous/test_comment.py | 159 ++ test/asynchronous/test_connection_logging.py | 45 + test/asynchronous/test_crud_unified.py | 39 + test/asynchronous/test_cursor.py | 14 +- test/asynchronous/test_encryption.py | 252 +-- test/asynchronous/unified_format.py | 1579 +++++++++++++++++ test/asynchronous/utils_spec_runner.py | 172 +- .../spec/legacy/timeoutMS.json | 4 +- test/test_client.py | 18 +- test/test_command_logging.py | 9 +- test/test_command_monitoring.py | 8 +- test/test_comment.py | 60 +- test/test_connection_logging.py | 8 +- test/test_connection_monitoring.py | 3 +- test/test_crud_unified.py | 10 +- test/test_cursor.py | 14 +- test/test_encryption.py | 250 +-- test/test_replica_set_reconfig.py | 3 +- test/test_server_selection_in_window.py | 2 +- test/unified_format.py | 703 +------- test/unified_format_shared.py | 674 +++++++ test/utils.py | 147 -- test/utils_spec_runner.py | 170 +- tools/synchro.py | 9 + 38 files changed, 3709 insertions(+), 1200 deletions(-) create mode 100644 .evergreen/scripts/generate_config.py create mode 100644 test/asynchronous/test_command_logging.py create mode 100644 test/asynchronous/test_command_monitoring.py create mode 100644 test/asynchronous/test_comment.py create mode 100644 test/asynchronous/test_connection_logging.py create mode 100644 test/asynchronous/test_crud_unified.py create mode 100644 test/asynchronous/unified_format.py create mode 100644 test/unified_format_shared.py diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 1ef8751501..dee4b608ec 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2826,42 +2826,150 @@ buildvariants: - "test-6.0-standalone" - "test-5.0-standalone" -- matrix_name: "ocsp-test" - matrix_spec: - platform: rhel8 - python-version: ["3.9", "3.10", "pypy3.9", "pypy3.10"] - mongodb-version: ["4.4", "5.0", "6.0", "7.0", "8.0", "latest"] - auth: "noauth" - ssl: "ssl" - display_name: "OCSP test ${platform} ${python-version} ${mongodb-version}" - batchtime: 20160 # 14 days +# OCSP test matrix. +- name: ocsp-test-rhel8-v4.4-py3.9 tasks: - - name: ".ocsp" - -- matrix_name: "ocsp-test-windows" - matrix_spec: - platform: windows - python-version-windows: ["3.9", "3.10"] - mongodb-version: ["4.4", "5.0", "6.0", "7.0", "8.0", "latest"] - auth: "noauth" - ssl: "ssl" - display_name: "OCSP test ${platform} ${python-version-windows} ${mongodb-version}" - batchtime: 20160 # 14 days + - name: .ocsp + display_name: OCSP test RHEL8 v4.4 py3.9 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: ocsp-test-rhel8-v5.0-py3.10 tasks: - # Windows MongoDB servers do not staple OCSP responses and only support RSA. - - name: ".ocsp-rsa !.ocsp-staple" - -- matrix_name: "ocsp-test-macos" - matrix_spec: - platform: macos - mongodb-version: ["4.4", "5.0", "6.0", "7.0", "8.0", "latest"] - auth: "noauth" - ssl: "ssl" - display_name: "OCSP test ${platform} ${mongodb-version}" - batchtime: 20160 # 14 days + - name: .ocsp + display_name: OCSP test RHEL8 v5.0 py3.10 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "5.0" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: ocsp-test-rhel8-v6.0-py3.11 tasks: - # macOS MongoDB servers do not staple OCSP responses and only support RSA. - - name: ".ocsp-rsa !.ocsp-staple" + - name: .ocsp + display_name: OCSP test RHEL8 v6.0 py3.11 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "6.0" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: ocsp-test-rhel8-v7.0-py3.12 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v7.0 py3.12 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "7.0" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: ocsp-test-rhel8-v8.0-py3.13 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v8.0 py3.13 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: ocsp-test-rhel8-rapid-pypy3.9 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 rapid pypy3.9 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: rapid + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: ocsp-test-rhel8-latest-pypy3.10 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 latest pypy3.10 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: latest + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: ocsp-test-win64-v4.4-py3.9 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test Win64 v4.4 py3.9 + run_on: + - windows-64-vsMulti-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: ocsp-test-win64-v8.0-py3.13 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test Win64 v8.0 py3.13 + run_on: + - windows-64-vsMulti-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: ocsp-test-macos-v4.4-py3.9 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test macOS v4.4 py3.9 + run_on: + - macos-14 + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: ocsp-test-macos-v8.0-py3.13 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test macOS v8.0 py3.13 + run_on: + - macos-14 + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - matrix_name: "oidc-auth-test" matrix_spec: diff --git a/.evergreen/hatch.sh b/.evergreen/hatch.sh index db0da2f4d0..6f3d36b389 100644 --- a/.evergreen/hatch.sh +++ b/.evergreen/hatch.sh @@ -18,17 +18,25 @@ if [ -n "$SKIP_HATCH" ]; then run_hatch() { bash ./.evergreen/run-tests.sh } -elif $PYTHON_BINARY -m hatch --version; then - run_hatch() { - $PYTHON_BINARY -m hatch run "$@" - } -else # No toolchain hatch present, set up virtualenv before installing hatch +else # Set up virtualenv before installing hatch # Use a random venv name because the encryption tasks run this script multiple times in the same run. ENV_NAME=hatchenv-$RANDOM createvirtualenv "$PYTHON_BINARY" $ENV_NAME # shellcheck disable=SC2064 trap "deactivate; rm -rf $ENV_NAME" EXIT HUP python -m pip install -q hatch + + # Ensure hatch does not write to user or global locations. + touch hatch_config.toml + HATCH_CONFIG=$(pwd)/hatch_config.toml + if [ "Windows_NT" = "$OS" ]; then # Magic variable in cygwin + HATCH_CONFIG=$(cygpath -m "$HATCH_CONFIG") + fi + export HATCH_CONFIG + hatch config restore + hatch config set dirs.data ".hatch/data" + hatch config set dirs.cache ".hatch/cache" + run_hatch() { python -m hatch run "$@" } diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 8d7a9f082a..5e8429dd28 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -257,9 +257,9 @@ if [ -z "$GREEN_FRAMEWORK" ]; then # Use --capture=tee-sys so pytest prints test output inline: # https://docs.pytest.org/en/stable/how-to/capture-stdout-stderr.html if [ -z "$TEST_SUITES" ]; then - python -m pytest -v --capture=tee-sys --durations=5 --maxfail=10 $TEST_ARGS + python -m pytest -v --capture=tee-sys --durations=5 $TEST_ARGS else - python -m pytest -v --capture=tee-sys --durations=5 --maxfail=10 -m $TEST_SUITES $TEST_ARGS + python -m pytest -v --capture=tee-sys --durations=5 -m $TEST_SUITES $TEST_ARGS fi else python green_framework_test.py $GREEN_FRAMEWORK -v $TEST_ARGS diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py new file mode 100644 index 0000000000..e98e527b72 --- /dev/null +++ b/.evergreen/scripts/generate_config.py @@ -0,0 +1,167 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "shrub.py>=3.2.0", +# "pyyaml>=6.0.2" +# ] +# /// + +# Note: Run this file with `hatch run`, `pipx run`, or `uv run`. +from __future__ import annotations + +from dataclasses import dataclass +from itertools import cycle, product, zip_longest +from typing import Any + +from shrub.v3.evg_build_variant import BuildVariant +from shrub.v3.evg_project import EvgProject +from shrub.v3.evg_task import EvgTaskRef +from shrub.v3.shrub_service import ShrubService + +############## +# Globals +############## + +ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"] +CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] +PYPYS = ["pypy3.9", "pypy3.10"] +ALL_PYTHONS = CPYTHONS + PYPYS +BATCHTIME_WEEK = 10080 +HOSTS = dict() + + +@dataclass +class Host: + name: str + run_on: str + display_name: str + + +HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8") +HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64") +HOSTS["macos"] = Host("macos", "macos-14", "macOS") + + +############## +# Helpers +############## + + +def create_variant( + task_names: list[str], + display_name: str, + *, + python: str | None = None, + version: str | None = None, + host: str | None = None, + **kwargs: Any, +) -> BuildVariant: + """Create a build variant for the given inputs.""" + task_refs = [EvgTaskRef(name=n) for n in task_names] + kwargs.setdefault("expansions", dict()) + expansions = kwargs.pop("expansions", dict()).copy() + host = host or "rhel8" + run_on = [HOSTS[host].run_on] + name = display_name.replace(" ", "-").lower() + if python: + expansions["PYTHON_BINARY"] = get_python_binary(python, host) + if version: + expansions["VERSION"] = version + expansions = expansions or None + return BuildVariant( + name=name, + display_name=display_name, + tasks=task_refs, + expansions=expansions, + run_on=run_on, + **kwargs, + ) + + +def get_python_binary(python: str, host: str) -> str: + """Get the appropriate python binary given a python version and host.""" + if host == "win64": + is_32 = python.startswith("32-bit") + if is_32: + _, python = python.split() + base = "C:/python/32" + else: + base = "C:/python" + python = python.replace(".", "") + return f"{base}/Python{python}/python.exe" + + if host == "rhel8": + return f"/opt/python/{python}/bin/python3" + + if host == "macos": + return f"/Library/Frameworks/Python.Framework/Versions/{python}/bin/python3" + + raise ValueError(f"no match found for python {python} on {host}") + + +def get_display_name(base: str, host: str, version: str, python: str) -> str: + """Get the display name of a variant.""" + if version not in ["rapid", "latest"]: + version = f"v{version}" + if not python.startswith("pypy"): + python = f"py{python}" + return f"{base} {HOSTS[host].display_name} {version} {python}" + + +def zip_cycle(*iterables, empty_default=None): + """Get all combinations of the inputs, cycling over the shorter list(s).""" + cycles = [cycle(i) for i in iterables] + for _ in zip_longest(*iterables): + yield tuple(next(i, empty_default) for i in cycles) + + +############## +# Variants +############## + + +def create_ocsp_variants() -> list[BuildVariant]: + variants = [] + batchtime = BATCHTIME_WEEK * 2 + expansions = dict(AUTH="noauth", SSL="ssl", TOPOLOGY="server") + base_display = "OCSP test" + + # OCSP tests on rhel8 with all servers v4.4+ and all python versions. + versions = [v for v in ALL_VERSIONS if v != "4.0"] + for version, python in zip_cycle(versions, ALL_PYTHONS): + host = "rhel8" + variant = create_variant( + [".ocsp"], + get_display_name(base_display, host, version, python), + python=python, + version=version, + host=host, + expansions=expansions, + batchtime=batchtime, + ) + variants.append(variant) + + # OCSP tests on Windows and MacOS. + # MongoDB servers on these hosts do not staple OCSP responses and only support RSA. + for host, version in product(["win64", "macos"], ["4.4", "8.0"]): + python = CPYTHONS[0] if version == "4.4" else CPYTHONS[-1] + variant = create_variant( + [".ocsp-rsa !.ocsp-staple"], + get_display_name(base_display, host, version, python), + python=python, + version=version, + host=host, + expansions=expansions, + batchtime=batchtime, + ) + variants.append(variant) + + return variants + + +################## +# Generate Config +################## + +project = EvgProject(tasks=None, buildvariants=create_ocsp_variants()) +print(ShrubService.generate_yaml(project)) # noqa: T201 diff --git a/pymongo/asynchronous/encryption.py b/pymongo/asynchronous/encryption.py index 9b00c13e10..735e543047 100644 --- a/pymongo/asynchronous/encryption.py +++ b/pymongo/asynchronous/encryption.py @@ -180,10 +180,20 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None: while kms_context.bytes_needed > 0: # CSOT: update timeout. conn.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0)) - data = conn.recv(kms_context.bytes_needed) + if _IS_SYNC: + data = conn.recv(kms_context.bytes_needed) + else: + from pymongo.network_layer import ( # type: ignore[attr-defined] + async_receive_data_socket, + ) + + data = await async_receive_data_socket(conn, kms_context.bytes_needed) if not data: raise OSError("KMS connection closed") kms_context.feed(data) + # Async raises an OSError instead of returning empty bytes + except OSError as err: + raise OSError("KMS connection closed") from err except BLOCKING_IO_ERRORS: raise socket.timeout("timed out") from None finally: diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index d2b45fd64a..eae2b0df4c 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -1453,13 +1453,6 @@ async def address(self) -> Optional[tuple[str, int]]: 'Cannot use "address" property when load balancing among' ' mongoses, use "nodes" instead.' ) - if topology_type not in ( - TOPOLOGY_TYPE.ReplicaSetWithPrimary, - TOPOLOGY_TYPE.Single, - TOPOLOGY_TYPE.LoadBalanced, - TOPOLOGY_TYPE.Sharded, - ): - return None return await self._server_property("address") @property diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index 4b57620d83..d14a21f41d 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -130,7 +130,7 @@ def _is_ready(fut: Future) -> None: loop.remove_writer(fd) async def _async_receive_ssl( - conn: _sslConn, length: int, loop: AbstractEventLoop + conn: _sslConn, length: int, loop: AbstractEventLoop, once: Optional[bool] = False ) -> memoryview: mv = memoryview(bytearray(length)) total_read = 0 @@ -145,6 +145,9 @@ def _is_ready(fut: Future) -> None: read = conn.recv_into(mv[total_read:]) if read == 0: raise OSError("connection closed") + # KMS responses update their expected size after the first batch, stop reading after one loop + if once: + return mv[:read] total_read += read except BLOCKING_IO_ERRORS as exc: fd = conn.fileno() @@ -275,6 +278,28 @@ async def async_receive_data( sock.settimeout(sock_timeout) +async def async_receive_data_socket( + sock: Union[socket.socket, _sslConn], length: int +) -> memoryview: + sock_timeout = sock.gettimeout() + timeout = sock_timeout + + sock.settimeout(0.0) + loop = asyncio.get_event_loop() + try: + if _HAVE_SSL and isinstance(sock, (SSLSocket, _sslConn)): + return await asyncio.wait_for( + _async_receive_ssl(sock, length, loop, once=True), # type: ignore[arg-type] + timeout=timeout, + ) + else: + return await asyncio.wait_for(_async_receive(sock, length, loop), timeout=timeout) # type: ignore[arg-type] + except asyncio.TimeoutError as err: + raise socket.timeout("timed out") from err + finally: + sock.settimeout(sock_timeout) + + async def _async_receive(conn: socket.socket, length: int, loop: AbstractEventLoop) -> memoryview: mv = memoryview(bytearray(length)) bytes_read = 0 diff --git a/pymongo/synchronous/encryption.py b/pymongo/synchronous/encryption.py index efef6df9e8..506ff8bcba 100644 --- a/pymongo/synchronous/encryption.py +++ b/pymongo/synchronous/encryption.py @@ -180,10 +180,20 @@ def kms_request(self, kms_context: MongoCryptKmsContext) -> None: while kms_context.bytes_needed > 0: # CSOT: update timeout. conn.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0)) - data = conn.recv(kms_context.bytes_needed) + if _IS_SYNC: + data = conn.recv(kms_context.bytes_needed) + else: + from pymongo.network_layer import ( # type: ignore[attr-defined] + receive_data_socket, + ) + + data = receive_data_socket(conn, kms_context.bytes_needed) if not data: raise OSError("KMS connection closed") kms_context.feed(data) + # Async raises an OSError instead of returning empty bytes + except OSError as err: + raise OSError("KMS connection closed") from err except BLOCKING_IO_ERRORS: raise socket.timeout("timed out") from None finally: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 8f4d9cacf2..7eab5e74f1 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -1447,13 +1447,6 @@ def address(self) -> Optional[tuple[str, int]]: 'Cannot use "address" property when load balancing among' ' mongoses, use "nodes" instead.' ) - if topology_type not in ( - TOPOLOGY_TYPE.ReplicaSetWithPrimary, - TOPOLOGY_TYPE.Single, - TOPOLOGY_TYPE.LoadBalanced, - TOPOLOGY_TYPE.Sharded, - ): - return None return self._server_property("address") @property diff --git a/test/__init__.py b/test/__init__.py index 6be3b49ce6..c55eb74c9d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -464,11 +464,12 @@ def wrap(*args, **kwargs): if not self.connected: pair = self.pair raise SkipTest(f"Cannot connect to MongoDB on {pair}") - if iscoroutinefunction(condition) and condition(): - if wraps_async: - return f(*args, **kwargs) - else: - return f(*args, **kwargs) + if iscoroutinefunction(condition): + if condition(): + if wraps_async: + return f(*args, **kwargs) + else: + return f(*args, **kwargs) elif condition(): if wraps_async: return f(*args, **kwargs) diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 1a386fe766..58e69c7c58 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -466,11 +466,12 @@ async def wrap(*args, **kwargs): if not self.connected: pair = await self.pair raise SkipTest(f"Cannot connect to MongoDB on {pair}") - if iscoroutinefunction(condition) and await condition(): - if wraps_async: - return await f(*args, **kwargs) - else: - return f(*args, **kwargs) + if iscoroutinefunction(condition): + if await condition(): + if wraps_async: + return await f(*args, **kwargs) + else: + return f(*args, **kwargs) elif condition(): if wraps_async: return await f(*args, **kwargs) diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index c4d71cdbe6..ce396997e3 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -837,8 +837,6 @@ async def test_init_disconnected(self): c = await self.async_rs_or_single_client(connect=False) self.assertIsInstance(c.topology_description, TopologyDescription) self.assertEqual(c.topology_description, c._topology._description) - self.assertIsNone(await c.address) # PYTHON-2981 - await c.admin.command("ping") # connect if async_client_context.is_rs: # The primary's host and port are from the replica set config. self.assertIsNotNone(await c.address) @@ -2027,6 +2025,22 @@ async def test_handshake_08_invalid_aws_ec2(self): None, ) + async def test_handshake_09_container_with_provider(self): + await self._test_handshake( + { + ENV_VAR_K8S: "1", + "AWS_LAMBDA_RUNTIME_API": "1", + "AWS_REGION": "us-east-1", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "256", + }, + { + "container": {"orchestrator": "kubernetes"}, + "name": "aws.lambda", + "region": "us-east-1", + "memory_mb": 256, + }, + ) + def test_dict_hints(self): self.db.t.find(hint={"x": 1}) diff --git a/test/asynchronous/test_command_logging.py b/test/asynchronous/test_command_logging.py new file mode 100644 index 0000000000..f9b459c152 --- /dev/null +++ b/test/asynchronous/test_command_logging.py @@ -0,0 +1,44 @@ +# Copyright 2023-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. + +"""Run the command monitoring unified format spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.asynchronous.unified_format import generate_test_classes + +_IS_SYNC = False + +# Location of JSON test specifications. +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_logging") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_logging") + + +globals().update( + generate_test_classes( + _TEST_PATH, + module=__name__, + ) +) + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_command_monitoring.py b/test/asynchronous/test_command_monitoring.py new file mode 100644 index 0000000000..311fd1fdc1 --- /dev/null +++ b/test/asynchronous/test_command_monitoring.py @@ -0,0 +1,45 @@ +# 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. + +"""Run the command monitoring unified format spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.asynchronous.unified_format import generate_test_classes + +_IS_SYNC = False + +# Location of JSON test specifications. +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_monitoring") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_monitoring") + + +globals().update( + generate_test_classes( + _TEST_PATH, + module=__name__, + ) +) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_comment.py b/test/asynchronous/test_comment.py new file mode 100644 index 0000000000..be3626a8b8 --- /dev/null +++ b/test/asynchronous/test_comment.py @@ -0,0 +1,159 @@ +# Copyright 2022-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 the keyword argument 'comment' in various helpers.""" + +from __future__ import annotations + +import inspect +import sys + +sys.path[0:0] = [""] +from asyncio import iscoroutinefunction +from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest +from test.utils import OvertCommandListener + +from bson.dbref import DBRef +from pymongo.asynchronous.command_cursor import AsyncCommandCursor +from pymongo.operations import IndexModel + +_IS_SYNC = False + + +class AsyncTestComment(AsyncIntegrationTest): + async def _test_ops( + self, + helpers, + already_supported, + listener, + ): + for h, args in helpers: + c = "testing comment with " + h.__name__ + with self.subTest("collection-" + h.__name__ + "-comment"): + for cc in [c, {"key": c}, ["any", 1]]: + listener.reset() + kwargs = {"comment": cc} + try: + maybe_cursor = await h(*args, **kwargs) + except Exception: + maybe_cursor = None + self.assertIn( + "comment", + inspect.signature(h).parameters, + msg="Could not find 'comment' in the " + "signature of function %s" % (h.__name__), + ) + self.assertEqual( + inspect.signature(h).parameters["comment"].annotation, "Optional[Any]" + ) + if isinstance(maybe_cursor, AsyncCommandCursor): + await maybe_cursor.close() + + cmd = listener.started_events[0] + self.assertEqual(cc, cmd.command.get("comment"), msg=cmd) + + if h.__name__ != "aggregate_raw_batches": + self.assertIn( + ":param comment:", + h.__doc__, + ) + if h not in already_supported: + self.assertIn( + "Added ``comment`` parameter", + h.__doc__, + ) + else: + self.assertNotIn( + "Added ``comment`` parameter", + h.__doc__, + ) + + listener.reset() + + @async_client_context.require_version_min(4, 7, -1) + @async_client_context.require_replica_set + async def test_database_helpers(self): + listener = OvertCommandListener() + db = (await self.async_rs_or_single_client(event_listeners=[listener])).db + helpers = [ + (db.watch, []), + (db.command, ["hello"]), + (db.list_collections, []), + (db.list_collection_names, []), + (db.drop_collection, ["hello"]), + (db.validate_collection, ["test"]), + (db.dereference, [DBRef("collection", 1)]), + ] + already_supported = [db.command, db.list_collections, db.list_collection_names] + await self._test_ops(helpers, already_supported, listener) + + @async_client_context.require_version_min(4, 7, -1) + @async_client_context.require_replica_set + async def test_client_helpers(self): + listener = OvertCommandListener() + cli = await self.async_rs_or_single_client(event_listeners=[listener]) + helpers = [ + (cli.watch, []), + (cli.list_databases, []), + (cli.list_database_names, []), + (cli.drop_database, ["test"]), + ] + already_supported = [ + cli.list_databases, + ] + await self._test_ops(helpers, already_supported, listener) + + @async_client_context.require_version_min(4, 7, -1) + async def test_collection_helpers(self): + listener = OvertCommandListener() + db = (await self.async_rs_or_single_client(event_listeners=[listener]))[self.db.name] + coll = db.get_collection("test") + + helpers = [ + (coll.list_indexes, []), + (coll.drop, []), + (coll.index_information, []), + (coll.options, []), + (coll.aggregate, [[{"$set": {"x": 1}}]]), + (coll.aggregate_raw_batches, [[{"$set": {"x": 1}}]]), + (coll.rename, ["temp_temp_temp"]), + (coll.distinct, ["_id"]), + (coll.find_one_and_delete, [{}]), + (coll.find_one_and_replace, [{}, {}]), + (coll.find_one_and_update, [{}, {"$set": {"a": 1}}]), + (coll.estimated_document_count, []), + (coll.count_documents, [{}]), + (coll.create_indexes, [[IndexModel("a")]]), + (coll.create_index, ["a"]), + (coll.drop_index, [[("a", 1)]]), + (coll.drop_indexes, []), + ] + already_supported = [ + coll.estimated_document_count, + coll.count_documents, + coll.create_indexes, + coll.drop_indexes, + coll.options, + coll.find_one_and_replace, + coll.drop_index, + coll.rename, + coll.distinct, + coll.find_one_and_delete, + coll.find_one_and_update, + ] + await self._test_ops(helpers, already_supported, listener) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_connection_logging.py b/test/asynchronous/test_connection_logging.py new file mode 100644 index 0000000000..6bc9835b70 --- /dev/null +++ b/test/asynchronous/test_connection_logging.py @@ -0,0 +1,45 @@ +# Copyright 2023-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. + +"""Run the connection logging unified format spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.unified_format import generate_test_classes + +_IS_SYNC = False + +# Location of JSON test specifications. +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "connection_logging") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "connection_logging") + + +globals().update( + generate_test_classes( + _TEST_PATH, + module=__name__, + ) +) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_crud_unified.py b/test/asynchronous/test_crud_unified.py new file mode 100644 index 0000000000..3d8deb36e9 --- /dev/null +++ b/test/asynchronous/test_crud_unified.py @@ -0,0 +1,39 @@ +# Copyright 2021-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 the CRUD unified spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.unified_format import generate_test_classes + +_IS_SYNC = False + +# Location of JSON test specifications. +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "crud", "unified") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "crud", "unified") + +# Generate unified tests. +globals().update(generate_test_classes(_TEST_PATH, module=__name__, RUN_ON_SERVERLESS=True)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_cursor.py b/test/asynchronous/test_cursor.py index f7b795cdae..b1ca8855de 100644 --- a/test/asynchronous/test_cursor.py +++ b/test/asynchronous/test_cursor.py @@ -1412,12 +1412,11 @@ async def test_to_list_length(self): self.assertEqual(len(docs), 2) async def test_to_list_csot_applied(self): - client = await self.async_single_client(timeoutMS=500) + client = await self.async_single_client(timeoutMS=500, w=1) + coll = client.pymongo.test # Initialize the client with a larger timeout to help make test less flakey with pymongo.timeout(10): - await client.admin.command("ping") - coll = client.pymongo.test - await coll.insert_many([{} for _ in range(5)]) + await coll.insert_many([{} for _ in range(5)]) cursor = coll.find({"$where": delay(1)}) with self.assertRaises(PyMongoError) as ctx: await cursor.to_list() @@ -1454,12 +1453,11 @@ async def test_command_cursor_to_list_length(self): @async_client_context.require_failCommand_blockConnection async def test_command_cursor_to_list_csot_applied(self): - client = await self.async_single_client(timeoutMS=500) + client = await self.async_single_client(timeoutMS=500, w=1) + coll = client.pymongo.test # Initialize the client with a larger timeout to help make test less flakey with pymongo.timeout(10): - await client.admin.command("ping") - coll = client.pymongo.test - await coll.insert_many([{} for _ in range(5)]) + await coll.insert_many([{} for _ in range(5)]) fail_command = { "configureFailPoint": "failCommand", "mode": {"times": 5}, diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index c3f6223384..88b005c4b3 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -30,6 +30,7 @@ import warnings from test.asynchronous import AsyncIntegrationTest, AsyncPyMongoTestCase, async_client_context from test.asynchronous.test_bulk import AsyncBulkTestBase +from test.asynchronous.utils_spec_runner import AsyncSpecRunner, AsyncSpecTestCreator from threading import Thread from typing import Any, Dict, Mapping, Optional @@ -59,7 +60,6 @@ from test.utils import ( AllowListEventListener, OvertCommandListener, - SpecTestCreator, TopologyEventListener, async_wait_until, camel_to_snake_args, @@ -626,132 +626,132 @@ async def test_with_statement(self): KMS_TLS_OPTS = {"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}} -if _IS_SYNC: - # TODO: Add asynchronous SpecRunner (https://jira.mongodb.org/browse/PYTHON-4700) - class TestSpec(AsyncSpecRunner): - @classmethod - @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") - def setUpClass(cls): - super().setUpClass() - - def parse_auto_encrypt_opts(self, opts): - """Parse clientOptions.autoEncryptOpts.""" - opts = camel_to_snake_args(opts) - kms_providers = opts["kms_providers"] - if "aws" in kms_providers: - kms_providers["aws"] = AWS_CREDS - if not any(AWS_CREDS.values()): - self.skipTest("AWS environment credentials are not set") - if "awsTemporary" in kms_providers: - kms_providers["aws"] = AWS_TEMP_CREDS - del kms_providers["awsTemporary"] - if not any(AWS_TEMP_CREDS.values()): - self.skipTest("AWS Temp environment credentials are not set") - if "awsTemporaryNoSessionToken" in kms_providers: - kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS - del kms_providers["awsTemporaryNoSessionToken"] - if not any(AWS_TEMP_NO_SESSION_CREDS.values()): - self.skipTest("AWS Temp environment credentials are not set") - if "azure" in kms_providers: - kms_providers["azure"] = AZURE_CREDS - if not any(AZURE_CREDS.values()): - self.skipTest("Azure environment credentials are not set") - if "gcp" in kms_providers: - kms_providers["gcp"] = GCP_CREDS - if not any(AZURE_CREDS.values()): - self.skipTest("GCP environment credentials are not set") - if "kmip" in kms_providers: - kms_providers["kmip"] = KMIP_CREDS - opts["kms_tls_options"] = KMS_TLS_OPTS - if "key_vault_namespace" not in opts: - opts["key_vault_namespace"] = "keyvault.datakeys" - if "extra_options" in opts: - opts.update(camel_to_snake_args(opts.pop("extra_options"))) - - opts = dict(opts) - return AutoEncryptionOpts(**opts) - - def parse_client_options(self, opts): - """Override clientOptions parsing to support autoEncryptOpts.""" - encrypt_opts = opts.pop("autoEncryptOpts", None) - if encrypt_opts: - opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts) - - return super().parse_client_options(opts) - - def get_object_name(self, op): - """Default object is collection.""" - return op.get("object", "collection") - - def maybe_skip_scenario(self, test): - super().maybe_skip_scenario(test) - desc = test["description"].lower() - if ( - "timeoutms applied to listcollections to get collection schema" in desc - and sys.platform in ("win32", "darwin") - ): - self.skipTest("PYTHON-3706 flaky test on Windows/macOS") - if "type=symbol" in desc: - self.skipTest("PyMongo does not support the symbol type") - - def setup_scenario(self, scenario_def): - """Override a test's setup.""" - key_vault_data = scenario_def["key_vault_data"] - encrypted_fields = scenario_def["encrypted_fields"] - json_schema = scenario_def["json_schema"] - data = scenario_def["data"] - coll = async_client_context.client.get_database("keyvault", codec_options=OPTS)[ - "datakeys" - ] - coll.delete_many({}) - if key_vault_data: - coll.insert_many(key_vault_data) - - db_name = self.get_scenario_db_name(scenario_def) - coll_name = self.get_scenario_coll_name(scenario_def) - db = async_client_context.client.get_database(db_name, codec_options=OPTS) - coll = db.drop_collection(coll_name, encrypted_fields=encrypted_fields) - wc = WriteConcern(w="majority") - kwargs: Dict[str, Any] = {} - if json_schema: - kwargs["validator"] = {"$jsonSchema": json_schema} - kwargs["codec_options"] = OPTS - if not data: - kwargs["write_concern"] = wc - if encrypted_fields: - kwargs["encryptedFields"] = encrypted_fields - db.create_collection(coll_name, **kwargs) - coll = db[coll_name] - if data: - # Load data. - coll.with_options(write_concern=wc).insert_many(scenario_def["data"]) - - def allowable_errors(self, op): - """Override expected error classes.""" - errors = super().allowable_errors(op) - # An updateOne test expects encryption to error when no $ operator - # appears but pymongo raises a client side ValueError in this case. - if op["name"] == "updateOne": - errors += (ValueError,) - return errors - - def create_test(scenario_def, test, name): - @async_client_context.require_test_commands - def run_scenario(self): - self.run_scenario(scenario_def, test) - - return run_scenario - - test_creator = SpecTestCreator(create_test, TestSpec, os.path.join(SPEC_PATH, "legacy")) - test_creator.create_tests() - - if _HAVE_PYMONGOCRYPT: - globals().update( - generate_test_classes( - os.path.join(SPEC_PATH, "unified"), - module=__name__, - ) +class AsyncTestSpec(AsyncSpecRunner): + @classmethod + @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") + async def _setup_class(cls): + await super()._setup_class() + + def parse_auto_encrypt_opts(self, opts): + """Parse clientOptions.autoEncryptOpts.""" + opts = camel_to_snake_args(opts) + kms_providers = opts["kms_providers"] + if "aws" in kms_providers: + kms_providers["aws"] = AWS_CREDS + if not any(AWS_CREDS.values()): + self.skipTest("AWS environment credentials are not set") + if "awsTemporary" in kms_providers: + kms_providers["aws"] = AWS_TEMP_CREDS + del kms_providers["awsTemporary"] + if not any(AWS_TEMP_CREDS.values()): + self.skipTest("AWS Temp environment credentials are not set") + if "awsTemporaryNoSessionToken" in kms_providers: + kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS + del kms_providers["awsTemporaryNoSessionToken"] + if not any(AWS_TEMP_NO_SESSION_CREDS.values()): + self.skipTest("AWS Temp environment credentials are not set") + if "azure" in kms_providers: + kms_providers["azure"] = AZURE_CREDS + if not any(AZURE_CREDS.values()): + self.skipTest("Azure environment credentials are not set") + if "gcp" in kms_providers: + kms_providers["gcp"] = GCP_CREDS + if not any(AZURE_CREDS.values()): + self.skipTest("GCP environment credentials are not set") + if "kmip" in kms_providers: + kms_providers["kmip"] = KMIP_CREDS + opts["kms_tls_options"] = KMS_TLS_OPTS + if "key_vault_namespace" not in opts: + opts["key_vault_namespace"] = "keyvault.datakeys" + if "extra_options" in opts: + opts.update(camel_to_snake_args(opts.pop("extra_options"))) + + opts = dict(opts) + return AutoEncryptionOpts(**opts) + + def parse_client_options(self, opts): + """Override clientOptions parsing to support autoEncryptOpts.""" + encrypt_opts = opts.pop("autoEncryptOpts", None) + if encrypt_opts: + opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts) + + return super().parse_client_options(opts) + + def get_object_name(self, op): + """Default object is collection.""" + return op.get("object", "collection") + + def maybe_skip_scenario(self, test): + super().maybe_skip_scenario(test) + desc = test["description"].lower() + if ( + "timeoutms applied to listcollections to get collection schema" in desc + and sys.platform in ("win32", "darwin") + ): + self.skipTest("PYTHON-3706 flaky test on Windows/macOS") + if "type=symbol" in desc: + self.skipTest("PyMongo does not support the symbol type") + if "timeoutms applied to listcollections to get collection schema" in desc and not _IS_SYNC: + self.skipTest("PYTHON-4844 flaky test on async") + + async def setup_scenario(self, scenario_def): + """Override a test's setup.""" + key_vault_data = scenario_def["key_vault_data"] + encrypted_fields = scenario_def["encrypted_fields"] + json_schema = scenario_def["json_schema"] + data = scenario_def["data"] + coll = async_client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"] + await coll.delete_many({}) + if key_vault_data: + await coll.insert_many(key_vault_data) + + db_name = self.get_scenario_db_name(scenario_def) + coll_name = self.get_scenario_coll_name(scenario_def) + db = async_client_context.client.get_database(db_name, codec_options=OPTS) + await db.drop_collection(coll_name, encrypted_fields=encrypted_fields) + wc = WriteConcern(w="majority") + kwargs: Dict[str, Any] = {} + if json_schema: + kwargs["validator"] = {"$jsonSchema": json_schema} + kwargs["codec_options"] = OPTS + if not data: + kwargs["write_concern"] = wc + if encrypted_fields: + kwargs["encryptedFields"] = encrypted_fields + await db.create_collection(coll_name, **kwargs) + coll = db[coll_name] + if data: + # Load data. + await coll.with_options(write_concern=wc).insert_many(scenario_def["data"]) + + def allowable_errors(self, op): + """Override expected error classes.""" + errors = super().allowable_errors(op) + # An updateOne test expects encryption to error when no $ operator + # appears but pymongo raises a client side ValueError in this case. + if op["name"] == "updateOne": + errors += (ValueError,) + return errors + + +async def create_test(scenario_def, test, name): + @async_client_context.require_test_commands + async def run_scenario(self): + await self.run_scenario(scenario_def, test) + + return run_scenario + + +test_creator = AsyncSpecTestCreator(create_test, AsyncTestSpec, os.path.join(SPEC_PATH, "legacy")) +test_creator.create_tests() + +if _HAVE_PYMONGOCRYPT: + globals().update( + generate_test_classes( + os.path.join(SPEC_PATH, "unified"), + module=__name__, ) + ) # Prose Tests ALL_KMS_PROVIDERS = { diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py new file mode 100644 index 0000000000..42bda59cb2 --- /dev/null +++ b/test/asynchronous/unified_format.py @@ -0,0 +1,1579 @@ +# Copyright 2020-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. + +"""Unified test format runner. + +https://github.com/mongodb/specifications/blob/master/source/unified-test-format/unified-test-format.rst +""" +from __future__ import annotations + +import asyncio +import binascii +import copy +import functools +import os +import re +import sys +import time +import traceback +from asyncio import iscoroutinefunction +from collections import defaultdict +from test.asynchronous import ( + AsyncIntegrationTest, + async_client_context, + client_knobs, + unittest, +) +from test.unified_format_shared import ( + KMS_TLS_OPTS, + PLACEHOLDER_MAP, + SKIP_CSOT_TESTS, + EventListenerUtil, + MatchEvaluatorUtil, + coerce_result, + parse_bulk_write_error_result, + parse_bulk_write_result, + parse_client_bulk_write_error_result, + parse_collection_or_database_options, + with_metaclass, +) +from test.utils import ( + async_get_pool, + camel_to_snake, + camel_to_snake_args, + parse_spec_options, + prepare_spec_arguments, + snake_to_camel, + wait_until, +) +from test.utils_spec_runner import SpecRunnerThread +from test.version import Version +from typing import Any, Dict, List, Mapping, Optional + +import pymongo +from bson import SON, json_util +from bson.codec_options import DEFAULT_CODEC_OPTIONS +from bson.objectid import ObjectId +from gridfs import AsyncGridFSBucket, GridOut +from pymongo import ASCENDING, AsyncMongoClient, CursorType, _csot +from pymongo.asynchronous.change_stream import AsyncChangeStream +from pymongo.asynchronous.client_session import AsyncClientSession, TransactionOptions, _TxnState +from pymongo.asynchronous.collection import AsyncCollection +from pymongo.asynchronous.command_cursor import AsyncCommandCursor +from pymongo.asynchronous.database import AsyncDatabase +from pymongo.asynchronous.encryption import AsyncClientEncryption +from pymongo.asynchronous.helpers import anext +from pymongo.encryption_options import _HAVE_PYMONGOCRYPT +from pymongo.errors import ( + BulkWriteError, + ClientBulkWriteException, + ConfigurationError, + ConnectionFailure, + EncryptionError, + InvalidOperation, + NotPrimaryError, + OperationFailure, + PyMongoError, +) +from pymongo.monitoring import ( + CommandStartedEvent, +) +from pymongo.operations import ( + SearchIndexModel, +) +from pymongo.read_concern import ReadConcern +from pymongo.read_preferences import ReadPreference +from pymongo.server_api import ServerApi +from pymongo.server_selectors import Selection, writable_server_selector +from pymongo.server_type import SERVER_TYPE +from pymongo.topology_description import TopologyDescription +from pymongo.typings import _Address +from pymongo.write_concern import WriteConcern + +_IS_SYNC = False + +IS_INTERRUPTED = False + + +def interrupt_loop(): + global IS_INTERRUPTED + IS_INTERRUPTED = True + + +async def is_run_on_requirement_satisfied(requirement): + topology_satisfied = True + req_topologies = requirement.get("topologies") + if req_topologies: + topology_satisfied = await async_client_context.is_topology_type(req_topologies) + + server_version = Version(*async_client_context.version[:3]) + + min_version_satisfied = True + req_min_server_version = requirement.get("minServerVersion") + if req_min_server_version: + min_version_satisfied = Version.from_string(req_min_server_version) <= server_version + + max_version_satisfied = True + req_max_server_version = requirement.get("maxServerVersion") + if req_max_server_version: + max_version_satisfied = Version.from_string(req_max_server_version) >= server_version + + serverless = requirement.get("serverless") + if serverless == "require": + serverless_satisfied = async_client_context.serverless + elif serverless == "forbid": + serverless_satisfied = not async_client_context.serverless + else: # unset or "allow" + serverless_satisfied = True + + params_satisfied = True + params = requirement.get("serverParameters") + if params: + for param, val in params.items(): + if param not in async_client_context.server_parameters: + params_satisfied = False + elif async_client_context.server_parameters[param] != val: + params_satisfied = False + + auth_satisfied = True + req_auth = requirement.get("auth") + if req_auth is not None: + if req_auth: + auth_satisfied = async_client_context.auth_enabled + if auth_satisfied and "authMechanism" in requirement: + auth_satisfied = async_client_context.check_auth_type(requirement["authMechanism"]) + else: + auth_satisfied = not async_client_context.auth_enabled + + csfle_satisfied = True + req_csfle = requirement.get("csfle") + if req_csfle is True: + min_version_satisfied = Version.from_string("4.2") <= server_version + csfle_satisfied = _HAVE_PYMONGOCRYPT and min_version_satisfied + + return ( + topology_satisfied + and min_version_satisfied + and max_version_satisfied + and serverless_satisfied + and params_satisfied + and auth_satisfied + and csfle_satisfied + ) + + +class NonLazyCursor: + """A find cursor proxy that creates the remote cursor when initialized.""" + + def __init__(self, find_cursor, client): + self.client = client + self.find_cursor = find_cursor + # Create the server side cursor. + self.first_result = None + + @classmethod + async def create(cls, find_cursor, client): + cursor = cls(find_cursor, client) + try: + cursor.first_result = await anext(cursor.find_cursor) + except StopAsyncIteration: + cursor.first_result = None + return cursor + + @property + def alive(self): + return self.first_result is not None or self.find_cursor.alive + + async def __anext__(self): + if self.first_result is not None: + first = self.first_result + self.first_result = None + return first + return await anext(self.find_cursor) + + # Added to support the iterateOnce operation. + try_next = __anext__ + + async def close(self): + await self.find_cursor.close() + self.client = None + + +class EntityMapUtil: + """Utility class that implements an entity map as per the unified + test format specification. + """ + + def __init__(self, test_class): + self._entities: Dict[str, Any] = {} + self._listeners: Dict[str, EventListenerUtil] = {} + self._session_lsids: Dict[str, Mapping[str, Any]] = {} + self.test: UnifiedSpecTestMixinV1 = test_class + self._cluster_time: Mapping[str, Any] = {} + + def __contains__(self, item): + return item in self._entities + + def __len__(self): + return len(self._entities) + + def __getitem__(self, item): + try: + return self._entities[item] + except KeyError: + self.test.fail(f"Could not find entity named {item} in map") + + def __setitem__(self, key, value): + if not isinstance(key, str): + self.test.fail("Expected entity name of type str, got %s" % (type(key))) + + if key in self._entities: + self.test.fail(f"Entity named {key} already in map") + + self._entities[key] = value + + def _handle_placeholders(self, spec: dict, current: dict, path: str) -> Any: + if "$$placeholder" in current: + if path not in PLACEHOLDER_MAP: + raise ValueError(f"Could not find a placeholder value for {path}") + return PLACEHOLDER_MAP[path] + + for key in list(current): + value = current[key] + if isinstance(value, dict): + subpath = f"{path}/{key}" + current[key] = self._handle_placeholders(spec, value, subpath) + return current + + async def _create_entity(self, entity_spec, uri=None): + if len(entity_spec) != 1: + self.test.fail(f"Entity spec {entity_spec} did not contain exactly one top-level key") + + entity_type, spec = next(iter(entity_spec.items())) + spec = self._handle_placeholders(spec, spec, "") + if entity_type == "client": + kwargs: dict = {} + observe_events = spec.get("observeEvents", []) + + # The unified tests use topologyOpeningEvent, we use topologyOpenedEvent + for i in range(len(observe_events)): + if "topologyOpeningEvent" == observe_events[i]: + observe_events[i] = "topologyOpenedEvent" + ignore_commands = spec.get("ignoreCommandMonitoringEvents", []) + observe_sensitive_commands = spec.get("observeSensitiveCommands", False) + ignore_commands = [cmd.lower() for cmd in ignore_commands] + listener = EventListenerUtil( + observe_events, + ignore_commands, + observe_sensitive_commands, + spec.get("storeEventsAsEntities"), + self, + ) + self._listeners[spec["id"]] = listener + kwargs["event_listeners"] = [listener] + if spec.get("useMultipleMongoses"): + if async_client_context.load_balancer or async_client_context.serverless: + kwargs["h"] = async_client_context.MULTI_MONGOS_LB_URI + elif async_client_context.is_mongos: + kwargs["h"] = async_client_context.mongos_seeds() + kwargs.update(spec.get("uriOptions", {})) + server_api = spec.get("serverApi") + if "waitQueueSize" in kwargs: + raise unittest.SkipTest("PyMongo does not support waitQueueSize") + if "waitQueueMultiple" in kwargs: + raise unittest.SkipTest("PyMongo does not support waitQueueMultiple") + if server_api: + kwargs["server_api"] = ServerApi( + server_api["version"], + strict=server_api.get("strict"), + deprecation_errors=server_api.get("deprecationErrors"), + ) + if uri: + kwargs["h"] = uri + client = await self.test.async_rs_or_single_client(**kwargs) + self[spec["id"]] = client + self.test.addAsyncCleanup(client.close) + return + elif entity_type == "database": + client = self[spec["client"]] + if type(client).__name__ != "AsyncMongoClient": + self.test.fail( + "Expected entity {} to be of type AsyncMongoClient, got {}".format( + spec["client"], type(client) + ) + ) + options = parse_collection_or_database_options(spec.get("databaseOptions", {})) + self[spec["id"]] = client.get_database(spec["databaseName"], **options) + return + elif entity_type == "collection": + database = self[spec["database"]] + if not isinstance(database, AsyncDatabase): + self.test.fail( + "Expected entity {} to be of type AsyncDatabase, got {}".format( + spec["database"], type(database) + ) + ) + options = parse_collection_or_database_options(spec.get("collectionOptions", {})) + self[spec["id"]] = database.get_collection(spec["collectionName"], **options) + return + elif entity_type == "session": + client = self[spec["client"]] + if type(client).__name__ != "AsyncMongoClient": + self.test.fail( + "Expected entity {} to be of type AsyncMongoClient, got {}".format( + spec["client"], type(client) + ) + ) + opts = camel_to_snake_args(spec.get("sessionOptions", {})) + if "default_transaction_options" in opts: + txn_opts = parse_spec_options(opts["default_transaction_options"]) + txn_opts = TransactionOptions(**txn_opts) + opts = copy.deepcopy(opts) + opts["default_transaction_options"] = txn_opts + session = client.start_session(**dict(opts)) + self[spec["id"]] = session + self._session_lsids[spec["id"]] = copy.deepcopy(session.session_id) + self.test.addAsyncCleanup(session.end_session) + return + elif entity_type == "bucket": + db = self[spec["database"]] + kwargs = parse_spec_options(spec.get("bucketOptions", {}).copy()) + bucket = AsyncGridFSBucket(db, **kwargs) + + # PyMongo does not support AsyncGridFSBucket.drop(), emulate it. + @_csot.apply + async def drop(self: AsyncGridFSBucket, *args: Any, **kwargs: Any) -> None: + await self._files.drop(*args, **kwargs) + await self._chunks.drop(*args, **kwargs) + + if not hasattr(bucket, "drop"): + bucket.drop = drop.__get__(bucket) + self[spec["id"]] = bucket + return + elif entity_type == "clientEncryption": + opts = camel_to_snake_args(spec["clientEncryptionOpts"].copy()) + if isinstance(opts["key_vault_client"], str): + opts["key_vault_client"] = self[opts["key_vault_client"]] + # Set TLS options for providers like "kmip:name1". + kms_tls_options = {} + for provider in opts["kms_providers"]: + provider_type = provider.split(":")[0] + if provider_type in KMS_TLS_OPTS: + kms_tls_options[provider] = KMS_TLS_OPTS[provider_type] + self[spec["id"]] = AsyncClientEncryption( + opts["kms_providers"], + opts["key_vault_namespace"], + opts["key_vault_client"], + DEFAULT_CODEC_OPTIONS, + opts.get("kms_tls_options", kms_tls_options), + ) + return + elif entity_type == "thread": + name = spec["id"] + thread = SpecRunnerThread(name) + thread.start() + self[name] = thread + return + + self.test.fail(f"Unable to create entity of unknown type {entity_type}") + + async def create_entities_from_spec(self, entity_spec, uri=None): + for spec in entity_spec: + await self._create_entity(spec, uri=uri) + + def get_listener_for_client(self, client_name: str) -> EventListenerUtil: + client = self[client_name] + if type(client).__name__ != "AsyncMongoClient": + self.test.fail( + f"Expected entity {client_name} to be of type AsyncMongoClient, got {type(client)}" + ) + + listener = self._listeners.get(client_name) + if not listener: + self.test.fail(f"No listeners configured for client {client_name}") + + return listener + + def get_lsid_for_session(self, session_name): + session = self[session_name] + if not isinstance(session, AsyncClientSession): + self.test.fail( + f"Expected entity {session_name} to be of type AsyncClientSession, got {type(session)}" + ) + + try: + return session.session_id + except InvalidOperation: + # session has been closed. + return self._session_lsids[session_name] + + async def advance_cluster_times(self) -> None: + """Manually synchronize entities when desired""" + if not self._cluster_time: + self._cluster_time = (await self.test.client.admin.command("ping")).get("$clusterTime") + for entity in self._entities.values(): + if isinstance(entity, AsyncClientSession) and self._cluster_time: + entity.advance_cluster_time(self._cluster_time) + + +class UnifiedSpecTestMixinV1(AsyncIntegrationTest): + """Mixin class to run test cases from test specification files. + + Assumes that tests conform to the `unified test format + `_. + + Specification of the test suite being currently run is available as + a class attribute ``TEST_SPEC``. + """ + + SCHEMA_VERSION = Version.from_string("1.21") + RUN_ON_LOAD_BALANCER = True + RUN_ON_SERVERLESS = True + TEST_SPEC: Any + mongos_clients: list[AsyncMongoClient] = [] + + @staticmethod + async def should_run_on(run_on_spec): + if not run_on_spec: + # Always run these tests. + return True + + for req in run_on_spec: + if await is_run_on_requirement_satisfied(req): + return True + return False + + async def insert_initial_data(self, initial_data): + for i, collection_data in enumerate(initial_data): + coll_name = collection_data["collectionName"] + db_name = collection_data["databaseName"] + opts = collection_data.get("createOptions", {}) + documents = collection_data["documents"] + + # Setup the collection with as few majority writes as possible. + db = self.client[db_name] + await db.drop_collection(coll_name) + # Only use majority wc only on the final write. + if i == len(initial_data) - 1: + wc = WriteConcern(w="majority") + else: + wc = WriteConcern(w=1) + if documents: + if opts: + await db.create_collection(coll_name, **opts) + await db.get_collection(coll_name, write_concern=wc).insert_many(documents) + else: + # Ensure collection exists + await db.create_collection(coll_name, write_concern=wc, **opts) + + @classmethod + async def _setup_class(cls): + # super call creates internal client cls.client + await super()._setup_class() + # process file-level runOnRequirements + run_on_spec = cls.TEST_SPEC.get("runOnRequirements", []) + if not await cls.should_run_on(run_on_spec): + raise unittest.SkipTest(f"{cls.__name__} runOnRequirements not satisfied") + + # add any special-casing for skipping tests here + if async_client_context.storage_engine == "mmapv1": + if "retryable-writes" in cls.TEST_SPEC["description"] or "retryable_writes" in str( + cls.TEST_PATH + ): + raise unittest.SkipTest("MMAPv1 does not support retryWrites=True") + + # Handle mongos_clients for transactions tests. + cls.mongos_clients = [] + if ( + async_client_context.supports_transactions() + and not async_client_context.load_balancer + and not async_client_context.serverless + ): + for address in async_client_context.mongoses: + cls.mongos_clients.append( + await cls.unmanaged_async_single_client("{}:{}".format(*address)) + ) + + # Speed up the tests by decreasing the heartbeat frequency. + cls.knobs = client_knobs( + heartbeat_frequency=0.1, + min_heartbeat_interval=0.1, + kill_cursor_frequency=0.1, + events_queue_frequency=0.1, + ) + cls.knobs.enable() + + @classmethod + async def _tearDown_class(cls): + cls.knobs.disable() + for client in cls.mongos_clients: + await client.close() + await super()._tearDown_class() + + async def asyncSetUp(self): + await super().asyncSetUp() + # process schemaVersion + # note: we check major schema version during class generation + # note: we do this here because we cannot run assertions in setUpClass + version = Version.from_string(self.TEST_SPEC["schemaVersion"]) + self.assertLessEqual( + version, + self.SCHEMA_VERSION, + f"expected schema version {self.SCHEMA_VERSION} or lower, got {version}", + ) + + # initialize internals + self.match_evaluator = MatchEvaluatorUtil(self) + + def maybe_skip_test(self, spec): + # add any special-casing for skipping tests here + if async_client_context.storage_engine == "mmapv1": + if ( + "Dirty explicit session is discarded" in spec["description"] + or "Dirty implicit session is discarded" in spec["description"] + or "Cancel server check" in spec["description"] + ): + self.skipTest("MMAPv1 does not support retryWrites=True") + if ( + "AsyncDatabase-level aggregate with $out includes read preference for 5.0+ server" + in spec["description"] + ): + if async_client_context.version[0] == 8: + self.skipTest("waiting on PYTHON-4356") + if "Aggregate with $out includes read preference for 5.0+ server" in spec["description"]: + if async_client_context.version[0] == 8: + self.skipTest("waiting on PYTHON-4356") + if "Client side error in command starting transaction" in spec["description"]: + self.skipTest("Implement PYTHON-1894") + if "timeoutMS applied to entire download" in spec["description"]: + self.skipTest("PyMongo's open_download_stream does not cap the stream's lifetime") + + class_name = self.__class__.__name__.lower() + description = spec["description"].lower() + if "csot" in class_name: + if "gridfs" in class_name and sys.platform == "win32": + self.skipTest("PYTHON-3522 CSOT GridFS tests are flaky on Windows") + if async_client_context.storage_engine == "mmapv1": + self.skipTest( + "MMAPv1 does not support retryable writes which is required for CSOT tests" + ) + if "change" in description or "change" in class_name: + self.skipTest("CSOT not implemented for watch()") + if "cursors" in class_name: + self.skipTest("CSOT not implemented for cursors") + if "tailable" in class_name: + self.skipTest("CSOT not implemented for tailable cursors") + if "sessions" in class_name: + self.skipTest("CSOT not implemented for sessions") + if "withtransaction" in description: + self.skipTest("CSOT not implemented for with_transaction") + if "transaction" in class_name or "transaction" in description: + self.skipTest("CSOT not implemented for transactions") + + # Some tests need to be skipped based on the operations they try to run. + for op in spec["operations"]: + name = op["name"] + if name == "count": + self.skipTest("PyMongo does not support count()") + if name == "listIndexNames": + self.skipTest("PyMongo does not support list_index_names()") + if async_client_context.storage_engine == "mmapv1": + if name == "createChangeStream": + self.skipTest("MMAPv1 does not support change streams") + if name == "withTransaction" or name == "startTransaction": + self.skipTest("MMAPv1 does not support document-level locking") + if not async_client_context.test_commands_enabled: + if name == "failPoint" or name == "targetedFailPoint": + self.skipTest("Test commands must be enabled to use fail points") + if name == "modifyCollection": + self.skipTest("PyMongo does not support modifyCollection") + if "timeoutMode" in op.get("arguments", {}): + self.skipTest("PyMongo does not support timeoutMode") + + def process_error(self, exception, spec): + if isinstance(exception, unittest.SkipTest): + raise + is_error = spec.get("isError") + is_client_error = spec.get("isClientError") + is_timeout_error = spec.get("isTimeoutError") + error_contains = spec.get("errorContains") + error_code = spec.get("errorCode") + error_code_name = spec.get("errorCodeName") + error_labels_contain = spec.get("errorLabelsContain") + error_labels_omit = spec.get("errorLabelsOmit") + expect_result = spec.get("expectResult") + error_response = spec.get("errorResponse") + if error_response: + if isinstance(exception, ClientBulkWriteException): + self.match_evaluator.match_result(error_response, exception.error.details) + else: + self.match_evaluator.match_result(error_response, exception.details) + + if is_error: + # already satisfied because exception was raised + pass + + if is_client_error: + if isinstance(exception, ClientBulkWriteException): + error = exception.error + else: + error = exception + # Connection errors are considered client errors. + if isinstance(error, ConnectionFailure): + self.assertNotIsInstance(error, NotPrimaryError) + elif isinstance(error, (InvalidOperation, ConfigurationError, EncryptionError)): + pass + else: + self.assertNotIsInstance(error, PyMongoError) + + if is_timeout_error: + self.assertIsInstance(exception, PyMongoError) + if not exception.timeout: + # Re-raise the exception for better diagnostics. + raise exception + + if error_contains: + if isinstance(exception, BulkWriteError): + errmsg = str(exception.details).lower() + elif isinstance(exception, ClientBulkWriteException): + errmsg = str(exception.details).lower() + else: + errmsg = str(exception).lower() + self.assertIn(error_contains.lower(), errmsg) + + if error_code: + if isinstance(exception, ClientBulkWriteException): + self.assertEqual(error_code, exception.error.details.get("code")) + else: + self.assertEqual(error_code, exception.details.get("code")) + + if error_code_name: + if isinstance(exception, ClientBulkWriteException): + self.assertEqual(error_code, exception.error.details.get("codeName")) + else: + self.assertEqual(error_code_name, exception.details.get("codeName")) + + if error_labels_contain: + if isinstance(exception, ClientBulkWriteException): + error = exception.error + else: + error = exception + labels = [ + err_label for err_label in error_labels_contain if error.has_error_label(err_label) + ] + self.assertEqual(labels, error_labels_contain) + + if error_labels_omit: + for err_label in error_labels_omit: + if exception.has_error_label(err_label): + self.fail(f"Exception '{exception}' unexpectedly had label '{err_label}'") + + if expect_result: + if isinstance(exception, BulkWriteError): + result = parse_bulk_write_error_result(exception) + self.match_evaluator.match_result(expect_result, result) + elif isinstance(exception, ClientBulkWriteException): + result = parse_client_bulk_write_error_result(exception) + self.match_evaluator.match_result(expect_result, result) + else: + self.fail( + f"expectResult can only be specified with {BulkWriteError} or {ClientBulkWriteException} exceptions" + ) + + return exception + + def __raise_if_unsupported(self, opname, target, *target_types): + if not isinstance(target, target_types): + self.fail(f"Operation {opname} not supported for entity of type {type(target)}") + + async def __entityOperation_createChangeStream(self, target, *args, **kwargs): + if async_client_context.storage_engine == "mmapv1": + self.skipTest("MMAPv1 does not support change streams") + self.__raise_if_unsupported( + "createChangeStream", target, AsyncMongoClient, AsyncDatabase, AsyncCollection + ) + stream = await target.watch(*args, **kwargs) + self.addAsyncCleanup(stream.close) + return stream + + async def _clientOperation_createChangeStream(self, target, *args, **kwargs): + return await self.__entityOperation_createChangeStream(target, *args, **kwargs) + + async def _databaseOperation_createChangeStream(self, target, *args, **kwargs): + return await self.__entityOperation_createChangeStream(target, *args, **kwargs) + + async def _collectionOperation_createChangeStream(self, target, *args, **kwargs): + return await self.__entityOperation_createChangeStream(target, *args, **kwargs) + + async def _databaseOperation_runCommand(self, target, **kwargs): + self.__raise_if_unsupported("runCommand", target, AsyncDatabase) + # Ensure the first key is the command name. + ordered_command = SON([(kwargs.pop("command_name"), 1)]) + ordered_command.update(kwargs["command"]) + kwargs["command"] = ordered_command + return await target.command(**kwargs) + + async def _databaseOperation_runCursorCommand(self, target, **kwargs): + return list(await self._databaseOperation_createCommandCursor(target, **kwargs)) + + async def _databaseOperation_createCommandCursor(self, target, **kwargs): + self.__raise_if_unsupported("createCommandCursor", target, AsyncDatabase) + # Ensure the first key is the command name. + ordered_command = SON([(kwargs.pop("command_name"), 1)]) + ordered_command.update(kwargs["command"]) + kwargs["command"] = ordered_command + batch_size = 0 + + cursor_type = kwargs.pop("cursor_type", "nonTailable") + if cursor_type == CursorType.TAILABLE: + ordered_command["tailable"] = True + elif cursor_type == CursorType.TAILABLE_AWAIT: + ordered_command["tailable"] = True + ordered_command["awaitData"] = True + elif cursor_type != "nonTailable": + self.fail(f"unknown cursorType: {cursor_type}") + + if "maxTimeMS" in kwargs: + kwargs["max_await_time_ms"] = kwargs.pop("maxTimeMS") + + if "batch_size" in kwargs: + batch_size = kwargs.pop("batch_size") + + cursor = await target.cursor_command(**kwargs) + + if batch_size > 0: + cursor.batch_size(batch_size) + + return cursor + + async def kill_all_sessions(self): + if getattr(self, "client", None) is None: + return + clients = self.mongos_clients if self.mongos_clients else [self.client] + for client in clients: + try: + await client.admin.command("killAllSessions", []) + except OperationFailure: + # "operation was interrupted" by killing the command's + # own session. + pass + + async def _databaseOperation_listCollections(self, target, *args, **kwargs): + if "batch_size" in kwargs: + kwargs["cursor"] = {"batchSize": kwargs.pop("batch_size")} + cursor = await target.list_collections(*args, **kwargs) + return list(cursor) + + async def _databaseOperation_createCollection(self, target, *args, **kwargs): + # PYTHON-1936 Ignore the listCollections event from create_collection. + kwargs["check_exists"] = False + ret = await target.create_collection(*args, **kwargs) + return ret + + async def __entityOperation_aggregate(self, target, *args, **kwargs): + self.__raise_if_unsupported("aggregate", target, AsyncDatabase, AsyncCollection) + return await (await target.aggregate(*args, **kwargs)).to_list() + + async def _databaseOperation_aggregate(self, target, *args, **kwargs): + return await self.__entityOperation_aggregate(target, *args, **kwargs) + + async def _collectionOperation_aggregate(self, target, *args, **kwargs): + return await self.__entityOperation_aggregate(target, *args, **kwargs) + + async def _collectionOperation_find(self, target, *args, **kwargs): + self.__raise_if_unsupported("find", target, AsyncCollection) + find_cursor = target.find(*args, **kwargs) + return await find_cursor.to_list() + + async def _collectionOperation_createFindCursor(self, target, *args, **kwargs): + self.__raise_if_unsupported("find", target, AsyncCollection) + if "filter" not in kwargs: + self.fail('createFindCursor requires a "filter" argument') + cursor = await NonLazyCursor.create(target.find(*args, **kwargs), target.database.client) + self.addAsyncCleanup(cursor.close) + return cursor + + def _collectionOperation_count(self, target, *args, **kwargs): + self.skipTest("PyMongo does not support collection.count()") + + async def _collectionOperation_listIndexes(self, target, *args, **kwargs): + if "batch_size" in kwargs: + self.skipTest("PyMongo does not support batch_size for list_indexes") + return await (await target.list_indexes(*args, **kwargs)).to_list() + + def _collectionOperation_listIndexNames(self, target, *args, **kwargs): + self.skipTest("PyMongo does not support list_index_names") + + async def _collectionOperation_createSearchIndexes(self, target, *args, **kwargs): + models = [SearchIndexModel(**i) for i in kwargs["models"]] + return await target.create_search_indexes(models) + + async def _collectionOperation_listSearchIndexes(self, target, *args, **kwargs): + name = kwargs.get("name") + agg_kwargs = kwargs.get("aggregation_options", dict()) + return await (await target.list_search_indexes(name, **agg_kwargs)).to_list() + + async def _sessionOperation_withTransaction(self, target, *args, **kwargs): + if async_client_context.storage_engine == "mmapv1": + self.skipTest("MMAPv1 does not support document-level locking") + self.__raise_if_unsupported("withTransaction", target, AsyncClientSession) + return await target.with_transaction(*args, **kwargs) + + async def _sessionOperation_startTransaction(self, target, *args, **kwargs): + if async_client_context.storage_engine == "mmapv1": + self.skipTest("MMAPv1 does not support document-level locking") + self.__raise_if_unsupported("startTransaction", target, AsyncClientSession) + return await target.start_transaction(*args, **kwargs) + + async def _changeStreamOperation_iterateUntilDocumentOrError(self, target, *args, **kwargs): + self.__raise_if_unsupported("iterateUntilDocumentOrError", target, AsyncChangeStream) + return await anext(target) + + async def _cursor_iterateUntilDocumentOrError(self, target, *args, **kwargs): + self.__raise_if_unsupported( + "iterateUntilDocumentOrError", target, NonLazyCursor, AsyncCommandCursor + ) + while target.alive: + try: + return await anext(target) + except StopAsyncIteration: + pass + return None + + async def _cursor_close(self, target, *args, **kwargs): + self.__raise_if_unsupported("close", target, NonLazyCursor, AsyncCommandCursor) + return await target.close() + + async def _clientEncryptionOperation_createDataKey(self, target, *args, **kwargs): + if "opts" in kwargs: + kwargs.update(camel_to_snake_args(kwargs.pop("opts"))) + + return await target.create_data_key(*args, **kwargs) + + async def _clientEncryptionOperation_getKeys(self, target, *args, **kwargs): + return await (await target.get_keys(*args, **kwargs)).to_list() + + async def _clientEncryptionOperation_deleteKey(self, target, *args, **kwargs): + result = await target.delete_key(*args, **kwargs) + response = result.raw_result + response["deletedCount"] = result.deleted_count + return response + + async def _clientEncryptionOperation_rewrapManyDataKey(self, target, *args, **kwargs): + if "opts" in kwargs: + kwargs.update(camel_to_snake_args(kwargs.pop("opts"))) + data = await target.rewrap_many_data_key(*args, **kwargs) + if data.bulk_write_result: + return {"bulkWriteResult": parse_bulk_write_result(data.bulk_write_result)} + return {} + + async def _clientEncryptionOperation_encrypt(self, target, *args, **kwargs): + if "opts" in kwargs: + kwargs.update(camel_to_snake_args(kwargs.pop("opts"))) + return await target.encrypt(*args, **kwargs) + + async def _bucketOperation_download( + self, target: AsyncGridFSBucket, *args: Any, **kwargs: Any + ) -> bytes: + async with await target.open_download_stream(*args, **kwargs) as gout: + return await gout.read() + + async def _bucketOperation_downloadByName( + self, target: AsyncGridFSBucket, *args: Any, **kwargs: Any + ) -> bytes: + async with await target.open_download_stream_by_name(*args, **kwargs) as gout: + return await gout.read() + + async def _bucketOperation_upload( + self, target: AsyncGridFSBucket, *args: Any, **kwargs: Any + ) -> ObjectId: + kwargs["source"] = binascii.unhexlify(kwargs.pop("source")["$$hexBytes"]) + if "content_type" in kwargs: + kwargs.setdefault("metadata", {})["contentType"] = kwargs.pop("content_type") + return await target.upload_from_stream(*args, **kwargs) + + async def _bucketOperation_uploadWithId( + self, target: AsyncGridFSBucket, *args: Any, **kwargs: Any + ) -> Any: + kwargs["source"] = binascii.unhexlify(kwargs.pop("source")["$$hexBytes"]) + if "content_type" in kwargs: + kwargs.setdefault("metadata", {})["contentType"] = kwargs.pop("content_type") + return await target.upload_from_stream_with_id(*args, **kwargs) + + async def _bucketOperation_find( + self, target: AsyncGridFSBucket, *args: Any, **kwargs: Any + ) -> List[GridOut]: + return await target.find(*args, **kwargs).to_list() + + async def run_entity_operation(self, spec): + target = self.entity_map[spec["object"]] + opname = spec["name"] + opargs = spec.get("arguments") + expect_error = spec.get("expectError") + save_as_entity = spec.get("saveResultAsEntity") + expect_result = spec.get("expectResult") + ignore = spec.get("ignoreResultAndError") + if ignore and (expect_error or save_as_entity or expect_result): + raise ValueError( + "ignoreResultAndError is incompatible with saveResultAsEntity" + ", expectError, and expectResult" + ) + if opargs: + arguments = parse_spec_options(copy.deepcopy(opargs)) + prepare_spec_arguments( + spec, + arguments, + camel_to_snake(opname), + self.entity_map, + self.run_operations_and_throw, + ) + else: + arguments = {} + + if isinstance(target, AsyncMongoClient): + method_name = f"_clientOperation_{opname}" + elif isinstance(target, AsyncDatabase): + method_name = f"_databaseOperation_{opname}" + elif isinstance(target, AsyncCollection): + method_name = f"_collectionOperation_{opname}" + # contentType is always stored in metadata in pymongo. + if target.name.endswith(".files") and opname == "find": + for doc in spec.get("expectResult", []): + if "contentType" in doc: + doc.setdefault("metadata", {})["contentType"] = doc.pop("contentType") + elif isinstance(target, AsyncChangeStream): + method_name = f"_changeStreamOperation_{opname}" + elif isinstance(target, (NonLazyCursor, AsyncCommandCursor)): + method_name = f"_cursor_{opname}" + elif isinstance(target, AsyncClientSession): + method_name = f"_sessionOperation_{opname}" + elif isinstance(target, AsyncGridFSBucket): + method_name = f"_bucketOperation_{opname}" + if "id" in arguments: + arguments["file_id"] = arguments.pop("id") + # MD5 is always disabled in pymongo. + arguments.pop("disable_md5", None) + elif isinstance(target, AsyncClientEncryption): + method_name = f"_clientEncryptionOperation_{opname}" + else: + method_name = "doesNotExist" + + try: + method = getattr(self, method_name) + except AttributeError: + target_opname = camel_to_snake(opname) + if target_opname == "iterate_once": + target_opname = "try_next" + if target_opname == "client_bulk_write": + target_opname = "bulk_write" + try: + cmd = getattr(target, target_opname) + except AttributeError: + self.fail(f"Unsupported operation {opname} on entity {target}") + else: + cmd = functools.partial(method, target) + + try: + # CSOT: Translate the spec test "timeout" arg into pymongo's context timeout API. + if "timeout" in arguments: + timeout = arguments.pop("timeout") + with pymongo.timeout(timeout): + result = await cmd(**dict(arguments)) + else: + result = await cmd(**dict(arguments)) + except Exception as exc: + # Ignore all operation errors but to avoid masking bugs don't + # ignore things like TypeError and ValueError. + if ignore and isinstance(exc, (PyMongoError,)): + return exc + if expect_error: + if method_name == "_collectionOperation_bulkWrite": + self.skipTest("Skipping test pending PYTHON-4598") + return self.process_error(exc, expect_error) + raise + else: + if method_name == "_collectionOperation_bulkWrite": + self.skipTest("Skipping test pending PYTHON-4598") + if expect_error: + self.fail(f'Excepted error {expect_error} but "{opname}" succeeded: {result}') + + if expect_result: + actual = coerce_result(opname, result) + self.match_evaluator.match_result(expect_result, actual) + + if save_as_entity: + self.entity_map[save_as_entity] = result + return None + return None + + async def __set_fail_point(self, client, command_args): + if not async_client_context.test_commands_enabled: + self.skipTest("Test commands must be enabled") + + cmd_on = SON([("configureFailPoint", "failCommand")]) + cmd_on.update(command_args) + await client.admin.command(cmd_on) + self.addAsyncCleanup( + client.admin.command, "configureFailPoint", cmd_on["configureFailPoint"], mode="off" + ) + + async def _testOperation_failPoint(self, spec): + await self.__set_fail_point( + client=self.entity_map[spec["client"]], command_args=spec["failPoint"] + ) + + async def _testOperation_targetedFailPoint(self, spec): + session = self.entity_map[spec["session"]] + if not session._pinned_address: + self.fail( + "Cannot use targetedFailPoint operation with unpinned " "session {}".format( + spec["session"] + ) + ) + + client = await self.async_single_client("{}:{}".format(*session._pinned_address)) + self.addAsyncCleanup(client.close) + await self.__set_fail_point(client=client, command_args=spec["failPoint"]) + + async def _testOperation_createEntities(self, spec): + await self.entity_map.create_entities_from_spec(spec["entities"], uri=self._uri) + await self.entity_map.advance_cluster_times() + + def _testOperation_assertSessionTransactionState(self, spec): + session = self.entity_map[spec["session"]] + expected_state = getattr(_TxnState, spec["state"].upper()) + self.assertEqual(expected_state, session._transaction.state) + + def _testOperation_assertSessionPinned(self, spec): + session = self.entity_map[spec["session"]] + self.assertIsNotNone(session._transaction.pinned_address) + + def _testOperation_assertSessionUnpinned(self, spec): + session = self.entity_map[spec["session"]] + self.assertIsNone(session._pinned_address) + self.assertIsNone(session._transaction.pinned_address) + + def __get_last_two_command_lsids(self, listener): + cmd_started_events = [] + for event in reversed(listener.events): + if isinstance(event, CommandStartedEvent): + cmd_started_events.append(event) + if len(cmd_started_events) < 2: + self.fail( + "Needed 2 CommandStartedEvents to compare lsids, " + "got %s" % (len(cmd_started_events)) + ) + return tuple([e.command["lsid"] for e in cmd_started_events][:2]) + + def _testOperation_assertDifferentLsidOnLastTwoCommands(self, spec): + listener = self.entity_map.get_listener_for_client(spec["client"]) + self.assertNotEqual(*self.__get_last_two_command_lsids(listener)) + + def _testOperation_assertSameLsidOnLastTwoCommands(self, spec): + listener = self.entity_map.get_listener_for_client(spec["client"]) + self.assertEqual(*self.__get_last_two_command_lsids(listener)) + + def _testOperation_assertSessionDirty(self, spec): + session = self.entity_map[spec["session"]] + self.assertTrue(session._server_session.dirty) + + def _testOperation_assertSessionNotDirty(self, spec): + session = self.entity_map[spec["session"]] + return self.assertFalse(session._server_session.dirty) + + async def _testOperation_assertCollectionExists(self, spec): + database_name = spec["databaseName"] + collection_name = spec["collectionName"] + collection_name_list = list( + await self.client.get_database(database_name).list_collection_names() + ) + self.assertIn(collection_name, collection_name_list) + + async def _testOperation_assertCollectionNotExists(self, spec): + database_name = spec["databaseName"] + collection_name = spec["collectionName"] + collection_name_list = list( + await self.client.get_database(database_name).list_collection_names() + ) + self.assertNotIn(collection_name, collection_name_list) + + async def _testOperation_assertIndexExists(self, spec): + collection = self.client[spec["databaseName"]][spec["collectionName"]] + index_names = [idx["name"] async for idx in await collection.list_indexes()] + self.assertIn(spec["indexName"], index_names) + + async def _testOperation_assertIndexNotExists(self, spec): + collection = self.client[spec["databaseName"]][spec["collectionName"]] + async for index in await collection.list_indexes(): + self.assertNotEqual(spec["indexName"], index["name"]) + + async def _testOperation_assertNumberConnectionsCheckedOut(self, spec): + client = self.entity_map[spec["client"]] + pool = await async_get_pool(client) + self.assertEqual(spec["connections"], pool.active_sockets) + + def _event_count(self, client_name, event): + listener = self.entity_map.get_listener_for_client(client_name) + actual_events = listener.get_events("all") + count = 0 + for actual in actual_events: + try: + self.match_evaluator.match_event(event, actual) + except AssertionError: + continue + else: + count += 1 + return count + + def _testOperation_assertEventCount(self, spec): + """Run the assertEventCount test operation. + + Assert the given event was published exactly `count` times. + """ + client, event, count = spec["client"], spec["event"], spec["count"] + self.assertEqual(self._event_count(client, event), count, f"expected {count} not {event!r}") + + def _testOperation_waitForEvent(self, spec): + """Run the waitForEvent test operation. + + Wait for a number of events to be published, or fail. + """ + client, event, count = spec["client"], spec["event"], spec["count"] + wait_until( + lambda: self._event_count(client, event) >= count, + f"find {count} {event} event(s)", + ) + + async def _testOperation_wait(self, spec): + """Run the "wait" test operation.""" + await asyncio.sleep(spec["ms"] / 1000.0) + + def _testOperation_recordTopologyDescription(self, spec): + """Run the recordTopologyDescription test operation.""" + self.entity_map[spec["id"]] = self.entity_map[spec["client"]].topology_description + + def _testOperation_assertTopologyType(self, spec): + """Run the assertTopologyType test operation.""" + description = self.entity_map[spec["topologyDescription"]] + self.assertIsInstance(description, TopologyDescription) + self.assertEqual(description.topology_type_name, spec["topologyType"]) + + def _testOperation_waitForPrimaryChange(self, spec: dict) -> None: + """Run the waitForPrimaryChange test operation.""" + client = self.entity_map[spec["client"]] + old_description: TopologyDescription = self.entity_map[spec["priorTopologyDescription"]] + timeout = spec["timeoutMS"] / 1000.0 + + def get_primary(td: TopologyDescription) -> Optional[_Address]: + servers = writable_server_selector(Selection.from_topology_description(td)) + if servers and servers[0].server_type == SERVER_TYPE.RSPrimary: + return servers[0].address + return None + + old_primary = get_primary(old_description) + + def primary_changed() -> bool: + primary = client.primary + if primary is None: + return False + return primary != old_primary + + wait_until(primary_changed, "change primary", timeout=timeout) + + def _testOperation_runOnThread(self, spec): + """Run the 'runOnThread' operation.""" + thread = self.entity_map[spec["thread"]] + thread.schedule(lambda: self.run_entity_operation(spec["operation"])) + + def _testOperation_waitForThread(self, spec): + """Run the 'waitForThread' operation.""" + thread = self.entity_map[spec["thread"]] + thread.stop() + thread.join(10) + if thread.exc: + raise thread.exc + self.assertFalse(thread.is_alive(), "Thread {} is still running".format(spec["thread"])) + + async def _testOperation_loop(self, spec): + failure_key = spec.get("storeFailuresAsEntity") + error_key = spec.get("storeErrorsAsEntity") + successes_key = spec.get("storeSuccessesAsEntity") + iteration_key = spec.get("storeIterationsAsEntity") + iteration_limiter_key = spec.get("numIterations") + for i in [failure_key, error_key]: + if i: + self.entity_map[i] = [] + for i in [successes_key, iteration_key]: + if i: + self.entity_map[i] = 0 + i = 0 + global IS_INTERRUPTED + while True: + if iteration_limiter_key and i >= iteration_limiter_key: + break + i += 1 + if IS_INTERRUPTED: + break + try: + if iteration_key: + self.entity_map._entities[iteration_key] += 1 + for op in spec["operations"]: + await self.run_entity_operation(op) + if successes_key: + self.entity_map._entities[successes_key] += 1 + except Exception as exc: + if isinstance(exc, AssertionError): + key = failure_key or error_key + else: + key = error_key or failure_key + if not key: + raise + self.entity_map[key].append( + {"error": str(exc), "time": time.time(), "type": type(exc).__name__} + ) + + async def run_special_operation(self, spec): + opname = spec["name"] + method_name = f"_testOperation_{opname}" + try: + method = getattr(self, method_name) + except AttributeError: + self.fail(f"Unsupported special test operation {opname}") + else: + if iscoroutinefunction(method): + await method(spec["arguments"]) + else: + method(spec["arguments"]) + + async def run_operations(self, spec): + for op in spec: + if op["object"] == "testRunner": + await self.run_special_operation(op) + else: + await self.run_entity_operation(op) + + async def run_operations_and_throw(self, spec): + for op in spec: + if op["object"] == "testRunner": + await self.run_special_operation(op) + else: + result = await self.run_entity_operation(op) + if isinstance(result, Exception): + raise result + + def check_events(self, spec): + for event_spec in spec: + client_name = event_spec["client"] + events = event_spec["events"] + event_type = event_spec.get("eventType", "command") + ignore_extra_events = event_spec.get("ignoreExtraEvents", False) + server_connection_id = event_spec.get("serverConnectionId") + has_server_connection_id = event_spec.get("hasServerConnectionId", False) + listener = self.entity_map.get_listener_for_client(client_name) + actual_events = listener.get_events(event_type) + if ignore_extra_events: + actual_events = actual_events[: len(events)] + + if len(events) == 0: + self.assertEqual(actual_events, []) + continue + + if len(actual_events) != len(events): + expected = "\n".join(str(e) for e in events) + actual = "\n".join(str(a) for a in actual_events) + self.assertEqual( + len(actual_events), + len(events), + f"expected events:\n{expected}\nactual events:\n{actual}", + ) + + for idx, expected_event in enumerate(events): + self.match_evaluator.match_event(expected_event, actual_events[idx]) + + if has_server_connection_id: + assert server_connection_id is not None + assert server_connection_id >= 0 + else: + assert server_connection_id is None + + def process_ignore_messages(self, ignore_logs, actual_logs): + final_logs = [] + for log in actual_logs: + ignored = False + for ignore_log in ignore_logs: + if log["data"]["message"] == ignore_log["data"][ + "message" + ] and self.match_evaluator.match_result(ignore_log, log, test=False): + ignored = True + break + if not ignored: + final_logs.append(log) + return final_logs + + async def check_log_messages(self, operations, spec): + def format_logs(log_list): + client_to_log = defaultdict(list) + for log in log_list: + if log.module == "ocsp_support": + continue + data = json_util.loads(log.getMessage()) + client = data.pop("clientId") if "clientId" in data else data.pop("topologyId") + client_to_log[client].append( + { + "level": log.levelname.lower(), + "component": log.name.replace("pymongo.", "", 1), + "data": data, + } + ) + return client_to_log + + with self.assertLogs("pymongo", level="DEBUG") as cm: + await self.run_operations(operations) + formatted_logs = format_logs(cm.records) + for client in spec: + components = set() + for message in client["messages"]: + components.add(message["component"]) + + clientid = self.entity_map[client["client"]]._topology_settings._topology_id + actual_logs = formatted_logs[clientid] + actual_logs = [log for log in actual_logs if log["component"] in components] + + ignore_logs = client.get("ignoreMessages", []) + if ignore_logs: + actual_logs = self.process_ignore_messages(ignore_logs, actual_logs) + + if client.get("ignoreExtraMessages", False): + actual_logs = actual_logs[: len(client["messages"])] + self.assertEqual( + len(client["messages"]), + len(actual_logs), + f"expected {client['messages']} but got {actual_logs}", + ) + for expected_msg, actual_msg in zip(client["messages"], actual_logs): + expected_data, actual_data = expected_msg.pop("data"), actual_msg.pop("data") + + if "failureIsRedacted" in expected_msg: + self.assertIn("failure", actual_data) + should_redact = expected_msg.pop("failureIsRedacted") + if should_redact: + actual_fields = set(json_util.loads(actual_data["failure"]).keys()) + self.assertTrue( + {"code", "codeName", "errorLabels"}.issuperset(actual_fields) + ) + + self.match_evaluator.match_result(expected_data, actual_data) + self.match_evaluator.match_result(expected_msg, actual_msg) + + async def verify_outcome(self, spec): + for collection_data in spec: + coll_name = collection_data["collectionName"] + db_name = collection_data["databaseName"] + expected_documents = collection_data["documents"] + + coll = self.client.get_database(db_name).get_collection( + coll_name, + read_preference=ReadPreference.PRIMARY, + read_concern=ReadConcern(level="local"), + ) + + if expected_documents: + sorted_expected_documents = sorted(expected_documents, key=lambda doc: doc["_id"]) + actual_documents = await coll.find({}, sort=[("_id", ASCENDING)]).to_list() + self.assertListEqual(sorted_expected_documents, actual_documents) + + async def run_scenario(self, spec, uri=None): + if "csot" in self.id().lower() and SKIP_CSOT_TESTS: + raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...") + + # Kill all sessions before and after each test to prevent an open + # transaction (from a test failure) from blocking collection/database + # operations during test set up and tear down. + await self.kill_all_sessions() + self.addAsyncCleanup(self.kill_all_sessions) + + if "csot" in self.id().lower(): + # Retry CSOT tests up to 2 times to deal with flakey tests. + attempts = 3 + for i in range(attempts): + try: + return await self._run_scenario(spec, uri) + except AssertionError: + if i < attempts - 1: + print( + f"Retrying after attempt {i+1} of {self.id()} failed with:\n" + f"{traceback.format_exc()}", + file=sys.stderr, + ) + await self.asyncSetUp() + continue + raise + return None + else: + await self._run_scenario(spec, uri) + return None + + async def _run_scenario(self, spec, uri=None): + # maybe skip test manually + self.maybe_skip_test(spec) + + # process test-level runOnRequirements + run_on_spec = spec.get("runOnRequirements", []) + if not await self.should_run_on(run_on_spec): + raise unittest.SkipTest("runOnRequirements not satisfied") + + # process skipReason + skip_reason = spec.get("skipReason", None) + if skip_reason is not None: + raise unittest.SkipTest(f"{skip_reason}") + + # process createEntities + self._uri = uri + self.entity_map = EntityMapUtil(self) + await self.entity_map.create_entities_from_spec( + self.TEST_SPEC.get("createEntities", []), uri=uri + ) + # process initialData + if "initialData" in self.TEST_SPEC: + await self.insert_initial_data(self.TEST_SPEC["initialData"]) + self._cluster_time = (await self.client.admin.command("ping")).get("$clusterTime") + await self.entity_map.advance_cluster_times() + + if "expectLogMessages" in spec: + expect_log_messages = spec["expectLogMessages"] + self.assertTrue(expect_log_messages, "expectEvents must be non-empty") + await self.check_log_messages(spec["operations"], expect_log_messages) + else: + # process operations + await self.run_operations(spec["operations"]) + + # process expectEvents + if "expectEvents" in spec: + expect_events = spec["expectEvents"] + self.assertTrue(expect_events, "expectEvents must be non-empty") + self.check_events(expect_events) + + # process outcome + await self.verify_outcome(spec.get("outcome", [])) + + +class UnifiedSpecTestMeta(type): + """Metaclass for generating test classes.""" + + TEST_SPEC: Any + EXPECTED_FAILURES: Any + + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + + def create_test(spec): + async def test_case(self): + await self.run_scenario(spec) + + return test_case + + for test_spec in cls.TEST_SPEC["tests"]: + description = test_spec["description"] + test_name = "test_{}".format( + description.strip(". ").replace(" ", "_").replace(".", "_") + ) + test_method = create_test(copy.deepcopy(test_spec)) + test_method.__name__ = str(test_name) + + for fail_pattern in cls.EXPECTED_FAILURES: + if re.search(fail_pattern, description): + test_method = unittest.expectedFailure(test_method) + break + + setattr(cls, test_name, test_method) + + +_ALL_MIXIN_CLASSES = [ + UnifiedSpecTestMixinV1, + # add mixin classes for new schema major versions here +] + + +_SCHEMA_VERSION_MAJOR_TO_MIXIN_CLASS = { + KLASS.SCHEMA_VERSION[0]: KLASS for KLASS in _ALL_MIXIN_CLASSES +} + + +def generate_test_classes( + test_path, + module=__name__, + class_name_prefix="", + expected_failures=[], # noqa: B006 + bypass_test_generation_errors=False, + **kwargs, +): + """Method for generating test classes. Returns a dictionary where keys are + the names of test classes and values are the test class objects. + """ + test_klasses = {} + + def test_base_class_factory(test_spec): + """Utility that creates the base class to use for test generation. + This is needed to ensure that cls.TEST_SPEC is appropriately set when + the metaclass __init__ is invoked. + """ + + class SpecTestBase(with_metaclass(UnifiedSpecTestMeta)): # type: ignore + TEST_SPEC = test_spec + EXPECTED_FAILURES = expected_failures + + return SpecTestBase + + for dirpath, _, filenames in os.walk(test_path): + dirname = os.path.split(dirpath)[-1] + + for filename in filenames: + fpath = os.path.join(dirpath, filename) + with open(fpath) as scenario_stream: + # Use tz_aware=False to match how CodecOptions decodes + # dates. + opts = json_util.JSONOptions(tz_aware=False) + scenario_def = json_util.loads(scenario_stream.read(), json_options=opts) + + test_type = os.path.splitext(filename)[0] + snake_class_name = "Test{}_{}_{}".format( + class_name_prefix, + dirname.replace("-", "_"), + test_type.replace("-", "_").replace(".", "_"), + ) + class_name = snake_to_camel(snake_class_name) + + try: + schema_version = Version.from_string(scenario_def["schemaVersion"]) + mixin_class = _SCHEMA_VERSION_MAJOR_TO_MIXIN_CLASS.get(schema_version[0]) + if mixin_class is None: + raise ValueError( + f"test file '{fpath}' has unsupported schemaVersion '{schema_version}'" + ) + module_dict = {"__module__": module, "TEST_PATH": test_path} + module_dict.update(kwargs) + test_klasses[class_name] = type( + class_name, + ( + mixin_class, + test_base_class_factory(scenario_def), + ), + module_dict, + ) + except Exception: + if bypass_test_generation_errors: + continue + raise + + return test_klasses diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index 12cb13c2cd..4d9c4c8f20 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -15,8 +15,12 @@ """Utilities for testing driver specs.""" from __future__ import annotations +import asyncio import functools +import os import threading +import unittest +from asyncio import iscoroutinefunction from collections import abc from test.asynchronous import AsyncIntegrationTest, async_client_context, client_knobs from test.utils import ( @@ -24,6 +28,7 @@ CompareType, EventListener, OvertCommandListener, + ScenarioDict, ServerAndTopologyEventListener, camel_to_snake, camel_to_snake_args, @@ -32,11 +37,12 @@ ) from typing import List -from bson import ObjectId, decode, encode +from bson import ObjectId, decode, encode, json_util from bson.binary import Binary from bson.int64 import Int64 from bson.son import SON from gridfs import GridFSBucket +from gridfs.asynchronous.grid_file import AsyncGridFSBucket from pymongo.asynchronous import client_session from pymongo.asynchronous.command_cursor import AsyncCommandCursor from pymongo.asynchronous.cursor import AsyncCursor @@ -83,6 +89,161 @@ def run(self): self.stop() +class AsyncSpecTestCreator: + """Class to create test cases from specifications.""" + + def __init__(self, create_test, test_class, test_path): + """Create a TestCreator object. + + :Parameters: + - `create_test`: callback that returns a test case. The callback + must accept the following arguments - a dictionary containing the + entire test specification (the `scenario_def`), a dictionary + containing the specification for which the test case will be + generated (the `test_def`). + - `test_class`: the unittest.TestCase class in which to create the + test case. + - `test_path`: path to the directory containing the JSON files with + the test specifications. + """ + self._create_test = create_test + self._test_class = test_class + self.test_path = test_path + + def _ensure_min_max_server_version(self, scenario_def, method): + """Test modifier that enforces a version range for the server on a + test case. + """ + if "minServerVersion" in scenario_def: + min_ver = tuple(int(elt) for elt in scenario_def["minServerVersion"].split(".")) + if min_ver is not None: + method = async_client_context.require_version_min(*min_ver)(method) + + if "maxServerVersion" in scenario_def: + max_ver = tuple(int(elt) for elt in scenario_def["maxServerVersion"].split(".")) + if max_ver is not None: + method = async_client_context.require_version_max(*max_ver)(method) + + if "serverless" in scenario_def: + serverless = scenario_def["serverless"] + if serverless == "require": + serverless_satisfied = async_client_context.serverless + elif serverless == "forbid": + serverless_satisfied = not async_client_context.serverless + else: # unset or "allow" + serverless_satisfied = True + method = unittest.skipUnless( + serverless_satisfied, "Serverless requirement not satisfied" + )(method) + + return method + + @staticmethod + async def valid_topology(run_on_req): + return await async_client_context.is_topology_type( + run_on_req.get("topology", ["single", "replicaset", "sharded", "load-balanced"]) + ) + + @staticmethod + def min_server_version(run_on_req): + version = run_on_req.get("minServerVersion") + if version: + min_ver = tuple(int(elt) for elt in version.split(".")) + return async_client_context.version >= min_ver + return True + + @staticmethod + def max_server_version(run_on_req): + version = run_on_req.get("maxServerVersion") + if version: + max_ver = tuple(int(elt) for elt in version.split(".")) + return async_client_context.version <= max_ver + return True + + @staticmethod + def valid_auth_enabled(run_on_req): + if "authEnabled" in run_on_req: + if run_on_req["authEnabled"]: + return async_client_context.auth_enabled + return not async_client_context.auth_enabled + return True + + @staticmethod + def serverless_ok(run_on_req): + serverless = run_on_req["serverless"] + if serverless == "require": + return async_client_context.serverless + elif serverless == "forbid": + return not async_client_context.serverless + else: # unset or "allow" + return True + + async def should_run_on(self, scenario_def): + run_on = scenario_def.get("runOn", []) + if not run_on: + # Always run these tests. + return True + + for req in run_on: + if ( + await self.valid_topology(req) + and self.min_server_version(req) + and self.max_server_version(req) + and self.valid_auth_enabled(req) + and self.serverless_ok(req) + ): + return True + return False + + def ensure_run_on(self, scenario_def, method): + """Test modifier that enforces a 'runOn' on a test case.""" + + async def predicate(): + return await self.should_run_on(scenario_def) + + return async_client_context._require(predicate, "runOn not satisfied", method) + + def tests(self, scenario_def): + """Allow CMAP spec test to override the location of test.""" + return scenario_def["tests"] + + async def _create_tests(self): + for dirpath, _, filenames in os.walk(self.test_path): + dirname = os.path.split(dirpath)[-1] + + for filename in filenames: + with open(os.path.join(dirpath, filename)) as scenario_stream: # noqa: ASYNC101, RUF100 + # Use tz_aware=False to match how CodecOptions decodes + # dates. + opts = json_util.JSONOptions(tz_aware=False) + scenario_def = ScenarioDict( + json_util.loads(scenario_stream.read(), json_options=opts) + ) + + test_type = os.path.splitext(filename)[0] + + # Construct test from scenario. + for test_def in self.tests(scenario_def): + test_name = "test_{}_{}_{}".format( + dirname, + test_type.replace("-", "_").replace(".", "_"), + str(test_def["description"].replace(" ", "_").replace(".", "_")), + ) + + new_test = await self._create_test(scenario_def, test_def, test_name) + new_test = self._ensure_min_max_server_version(scenario_def, new_test) + new_test = self.ensure_run_on(scenario_def, new_test) + + new_test.__name__ = test_name + setattr(self._test_class, new_test.__name__, new_test) + + def create_tests(self): + if _IS_SYNC: + self._create_tests() + else: + asyncio.run(self._create_tests()) + + class AsyncSpecRunner(AsyncIntegrationTest): mongos_clients: List knobs: client_knobs @@ -284,7 +445,7 @@ async def run_operation(self, sessions, collection, operation): if object_name == "gridfsbucket": # Only create the GridFSBucket when we need it (for the gridfs # retryable reads tests). - obj = GridFSBucket(database, bucket_name=collection.name) + obj = AsyncGridFSBucket(database, bucket_name=collection.name) else: objects = { "client": database.client, @@ -312,7 +473,10 @@ async def run_operation(self, sessions, collection, operation): args.update(arguments) arguments = args - result = cmd(**dict(arguments)) + if not _IS_SYNC and iscoroutinefunction(cmd): + result = await cmd(**dict(arguments)) + else: + result = cmd(**dict(arguments)) # Cleanup open change stream cursors. if name == "watch": self.addAsyncCleanup(result.close) @@ -588,7 +752,7 @@ async def run_scenario(self, scenario_def, test): read_preference=ReadPreference.PRIMARY, read_concern=ReadConcern("local"), ) - actual_data = await (await outcome_coll.find(sort=[("_id", 1)])).to_list() + actual_data = await outcome_coll.find(sort=[("_id", 1)]).to_list() # The expected data needs to be the left hand side here otherwise # CompareType(Binary) doesn't work. diff --git a/test/client-side-encryption/spec/legacy/timeoutMS.json b/test/client-side-encryption/spec/legacy/timeoutMS.json index b667767cfc..8411306224 100644 --- a/test/client-side-encryption/spec/legacy/timeoutMS.json +++ b/test/client-side-encryption/spec/legacy/timeoutMS.json @@ -110,7 +110,7 @@ "listCollections" ], "blockConnection": true, - "blockTimeMS": 60 + "blockTimeMS": 600 } }, "clientOptions": { @@ -119,7 +119,7 @@ "aws": {} } }, - "timeoutMS": 50 + "timeoutMS": 500 }, "operations": [ { diff --git a/test/test_client.py b/test/test_client.py index a4c521157b..07f3e560fe 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -812,8 +812,6 @@ def test_init_disconnected(self): c = self.rs_or_single_client(connect=False) self.assertIsInstance(c.topology_description, TopologyDescription) self.assertEqual(c.topology_description, c._topology._description) - self.assertIsNone(c.address) # PYTHON-2981 - c.admin.command("ping") # connect if client_context.is_rs: # The primary's host and port are from the replica set config. self.assertIsNotNone(c.address) @@ -1984,6 +1982,22 @@ def test_handshake_08_invalid_aws_ec2(self): None, ) + def test_handshake_09_container_with_provider(self): + self._test_handshake( + { + ENV_VAR_K8S: "1", + "AWS_LAMBDA_RUNTIME_API": "1", + "AWS_REGION": "us-east-1", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "256", + }, + { + "container": {"orchestrator": "kubernetes"}, + "name": "aws.lambda", + "region": "us-east-1", + "memory_mb": 256, + }, + ) + def test_dict_hints(self): self.db.t.find(hint={"x": 1}) diff --git a/test/test_command_logging.py b/test/test_command_logging.py index 9b2d52e66b..cf865920ca 100644 --- a/test/test_command_logging.py +++ b/test/test_command_logging.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -23,8 +24,14 @@ from test import unittest from test.unified_format import generate_test_classes +_IS_SYNC = True + # Location of JSON test specifications. -_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "command_logging") +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_logging") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_logging") + globals().update( generate_test_classes( diff --git a/test/test_command_monitoring.py b/test/test_command_monitoring.py index d2f578824d..4f5ef06f28 100644 --- a/test/test_command_monitoring.py +++ b/test/test_command_monitoring.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -23,8 +24,13 @@ from test import unittest from test.unified_format import generate_test_classes +_IS_SYNC = True + # Location of JSON test specifications. -_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "command_monitoring") +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "command_monitoring") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "command_monitoring") globals().update( diff --git a/test/test_comment.py b/test/test_comment.py index c0f037ea44..9f9bf98640 100644 --- a/test/test_comment.py +++ b/test/test_comment.py @@ -20,24 +20,15 @@ import sys sys.path[0:0] = [""] - +from asyncio import iscoroutinefunction from test import IntegrationTest, client_context, unittest -from test.utils import EventListener +from test.utils import OvertCommandListener from bson.dbref import DBRef from pymongo.operations import IndexModel from pymongo.synchronous.command_cursor import CommandCursor - -class Empty: - def __getattr__(self, item): - try: - self.__dict__[item] - except KeyError: - return self.empty - - def empty(self, *args, **kwargs): - return Empty() +_IS_SYNC = True class TestComment(IntegrationTest): @@ -46,8 +37,6 @@ def _test_ops( helpers, already_supported, listener, - db=Empty(), # noqa: B008 - coll=Empty(), # noqa: B008 ): for h, args in helpers: c = "testing comment with " + h.__name__ @@ -55,19 +44,10 @@ def _test_ops( for cc in [c, {"key": c}, ["any", 1]]: listener.reset() kwargs = {"comment": cc} - if h == coll.rename: - _ = db.get_collection("temp_temp_temp").drop() - destruct_coll = db.get_collection("test_temp") - destruct_coll.insert_one({}) - maybe_cursor = destruct_coll.rename(*args, **kwargs) - destruct_coll.drop() - elif h == db.validate_collection: - coll = db.get_collection("test") - coll.insert_one({}) - maybe_cursor = db.validate_collection(*args, **kwargs) - else: - coll.create_index("a") + try: maybe_cursor = h(*args, **kwargs) + except Exception: + maybe_cursor = None self.assertIn( "comment", inspect.signature(h).parameters, @@ -79,15 +59,11 @@ def _test_ops( ) if isinstance(maybe_cursor, CommandCursor): maybe_cursor.close() - tested = False - # For some reason collection.list_indexes creates two commands and the first - # one doesn't contain 'comment'. - for i in listener.started_events: - if cc == i.command.get("comment", ""): - self.assertEqual(cc, i.command["comment"]) - tested = True - self.assertTrue(tested) - if h not in [coll.aggregate_raw_batches]: + + cmd = listener.started_events[0] + self.assertEqual(cc, cmd.command.get("comment"), msg=cmd) + + if h.__name__ != "aggregate_raw_batches": self.assertIn( ":param comment:", h.__doc__, @@ -108,8 +84,8 @@ def _test_ops( @client_context.require_version_min(4, 7, -1) @client_context.require_replica_set def test_database_helpers(self): - listener = EventListener() - db = self.rs_or_single_client(event_listeners=[listener]).db + listener = OvertCommandListener() + db = (self.rs_or_single_client(event_listeners=[listener])).db helpers = [ (db.watch, []), (db.command, ["hello"]), @@ -120,12 +96,12 @@ def test_database_helpers(self): (db.dereference, [DBRef("collection", 1)]), ] already_supported = [db.command, db.list_collections, db.list_collection_names] - self._test_ops(helpers, already_supported, listener, db=db, coll=db.get_collection("test")) + self._test_ops(helpers, already_supported, listener) @client_context.require_version_min(4, 7, -1) @client_context.require_replica_set def test_client_helpers(self): - listener = EventListener() + listener = OvertCommandListener() cli = self.rs_or_single_client(event_listeners=[listener]) helpers = [ (cli.watch, []), @@ -140,8 +116,8 @@ def test_client_helpers(self): @client_context.require_version_min(4, 7, -1) def test_collection_helpers(self): - listener = EventListener() - db = self.rs_or_single_client(event_listeners=[listener])[self.db.name] + listener = OvertCommandListener() + db = (self.rs_or_single_client(event_listeners=[listener]))[self.db.name] coll = db.get_collection("test") helpers = [ @@ -176,7 +152,7 @@ def test_collection_helpers(self): coll.find_one_and_delete, coll.find_one_and_update, ] - self._test_ops(helpers, already_supported, listener, coll=coll, db=db) + self._test_ops(helpers, already_supported, listener) if __name__ == "__main__": diff --git a/test/test_connection_logging.py b/test/test_connection_logging.py index 262ce821eb..253193cc43 100644 --- a/test/test_connection_logging.py +++ b/test/test_connection_logging.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -23,8 +24,13 @@ from test import unittest from test.unified_format import generate_test_classes +_IS_SYNC = True + # Location of JSON test specifications. -_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "connection_logging") +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "connection_logging") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "connection_logging") globals().update( diff --git a/test/test_connection_monitoring.py b/test/test_connection_monitoring.py index 142af0f9a7..d576a1184a 100644 --- a/test/test_connection_monitoring.py +++ b/test/test_connection_monitoring.py @@ -25,14 +25,13 @@ from test.pymongo_mocks import DummyMonitor from test.utils import ( CMAPListener, - SpecTestCreator, camel_to_snake, client_context, get_pool, get_pools, wait_until, ) -from test.utils_spec_runner import SpecRunnerThread +from test.utils_spec_runner import SpecRunnerThread, SpecTestCreator from bson.objectid import ObjectId from bson.son import SON diff --git a/test/test_crud_unified.py b/test/test_crud_unified.py index 92a60a47fc..26f34cba88 100644 --- a/test/test_crud_unified.py +++ b/test/test_crud_unified.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -23,11 +24,16 @@ from test import unittest from test.unified_format import generate_test_classes +_IS_SYNC = True + # Location of JSON test specifications. -TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "crud", "unified") +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "crud", "unified") +else: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "crud", "unified") # Generate unified tests. -globals().update(generate_test_classes(TEST_PATH, module=__name__, RUN_ON_SERVERLESS=True)) +globals().update(generate_test_classes(_TEST_PATH, module=__name__, RUN_ON_SERVERLESS=True)) if __name__ == "__main__": unittest.main() diff --git a/test/test_cursor.py b/test/test_cursor.py index 7c073bf351..7a6dfc9429 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -1403,12 +1403,11 @@ def test_to_list_length(self): self.assertEqual(len(docs), 2) def test_to_list_csot_applied(self): - client = self.single_client(timeoutMS=500) + client = self.single_client(timeoutMS=500, w=1) + coll = client.pymongo.test # Initialize the client with a larger timeout to help make test less flakey with pymongo.timeout(10): - client.admin.command("ping") - coll = client.pymongo.test - coll.insert_many([{} for _ in range(5)]) + coll.insert_many([{} for _ in range(5)]) cursor = coll.find({"$where": delay(1)}) with self.assertRaises(PyMongoError) as ctx: cursor.to_list() @@ -1445,12 +1444,11 @@ def test_command_cursor_to_list_length(self): @client_context.require_failCommand_blockConnection def test_command_cursor_to_list_csot_applied(self): - client = self.single_client(timeoutMS=500) + client = self.single_client(timeoutMS=500, w=1) + coll = client.pymongo.test # Initialize the client with a larger timeout to help make test less flakey with pymongo.timeout(10): - client.admin.command("ping") - coll = client.pymongo.test - coll.insert_many([{} for _ in range(5)]) + coll.insert_many([{} for _ in range(5)]) fail_command = { "configureFailPoint": "failCommand", "mode": {"times": 5}, diff --git a/test/test_encryption.py b/test/test_encryption.py index 43c85e2c5b..13a69ca9ad 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -30,6 +30,7 @@ import warnings from test import IntegrationTest, PyMongoTestCase, client_context from test.test_bulk import BulkTestBase +from test.utils_spec_runner import SpecRunner, SpecTestCreator from threading import Thread from typing import Any, Dict, Mapping, Optional @@ -58,7 +59,6 @@ from test.utils import ( AllowListEventListener, OvertCommandListener, - SpecTestCreator, TopologyEventListener, camel_to_snake_args, is_greenthread_patched, @@ -624,130 +624,132 @@ def test_with_statement(self): KMS_TLS_OPTS = {"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}} -if _IS_SYNC: - # TODO: Add synchronous SpecRunner (https://jira.mongodb.org/browse/PYTHON-4700) - class TestSpec(SpecRunner): - @classmethod - @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") - def setUpClass(cls): - super().setUpClass() - - def parse_auto_encrypt_opts(self, opts): - """Parse clientOptions.autoEncryptOpts.""" - opts = camel_to_snake_args(opts) - kms_providers = opts["kms_providers"] - if "aws" in kms_providers: - kms_providers["aws"] = AWS_CREDS - if not any(AWS_CREDS.values()): - self.skipTest("AWS environment credentials are not set") - if "awsTemporary" in kms_providers: - kms_providers["aws"] = AWS_TEMP_CREDS - del kms_providers["awsTemporary"] - if not any(AWS_TEMP_CREDS.values()): - self.skipTest("AWS Temp environment credentials are not set") - if "awsTemporaryNoSessionToken" in kms_providers: - kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS - del kms_providers["awsTemporaryNoSessionToken"] - if not any(AWS_TEMP_NO_SESSION_CREDS.values()): - self.skipTest("AWS Temp environment credentials are not set") - if "azure" in kms_providers: - kms_providers["azure"] = AZURE_CREDS - if not any(AZURE_CREDS.values()): - self.skipTest("Azure environment credentials are not set") - if "gcp" in kms_providers: - kms_providers["gcp"] = GCP_CREDS - if not any(AZURE_CREDS.values()): - self.skipTest("GCP environment credentials are not set") - if "kmip" in kms_providers: - kms_providers["kmip"] = KMIP_CREDS - opts["kms_tls_options"] = KMS_TLS_OPTS - if "key_vault_namespace" not in opts: - opts["key_vault_namespace"] = "keyvault.datakeys" - if "extra_options" in opts: - opts.update(camel_to_snake_args(opts.pop("extra_options"))) - - opts = dict(opts) - return AutoEncryptionOpts(**opts) - - def parse_client_options(self, opts): - """Override clientOptions parsing to support autoEncryptOpts.""" - encrypt_opts = opts.pop("autoEncryptOpts", None) - if encrypt_opts: - opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts) - - return super().parse_client_options(opts) - - def get_object_name(self, op): - """Default object is collection.""" - return op.get("object", "collection") - - def maybe_skip_scenario(self, test): - super().maybe_skip_scenario(test) - desc = test["description"].lower() - if ( - "timeoutms applied to listcollections to get collection schema" in desc - and sys.platform in ("win32", "darwin") - ): - self.skipTest("PYTHON-3706 flaky test on Windows/macOS") - if "type=symbol" in desc: - self.skipTest("PyMongo does not support the symbol type") - - def setup_scenario(self, scenario_def): - """Override a test's setup.""" - key_vault_data = scenario_def["key_vault_data"] - encrypted_fields = scenario_def["encrypted_fields"] - json_schema = scenario_def["json_schema"] - data = scenario_def["data"] - coll = client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"] - coll.delete_many({}) - if key_vault_data: - coll.insert_many(key_vault_data) - - db_name = self.get_scenario_db_name(scenario_def) - coll_name = self.get_scenario_coll_name(scenario_def) - db = client_context.client.get_database(db_name, codec_options=OPTS) - coll = db.drop_collection(coll_name, encrypted_fields=encrypted_fields) - wc = WriteConcern(w="majority") - kwargs: Dict[str, Any] = {} - if json_schema: - kwargs["validator"] = {"$jsonSchema": json_schema} - kwargs["codec_options"] = OPTS - if not data: - kwargs["write_concern"] = wc - if encrypted_fields: - kwargs["encryptedFields"] = encrypted_fields - db.create_collection(coll_name, **kwargs) - coll = db[coll_name] - if data: - # Load data. - coll.with_options(write_concern=wc).insert_many(scenario_def["data"]) - - def allowable_errors(self, op): - """Override expected error classes.""" - errors = super().allowable_errors(op) - # An updateOne test expects encryption to error when no $ operator - # appears but pymongo raises a client side ValueError in this case. - if op["name"] == "updateOne": - errors += (ValueError,) - return errors - - def create_test(scenario_def, test, name): - @client_context.require_test_commands - def run_scenario(self): - self.run_scenario(scenario_def, test) - - return run_scenario - - test_creator = SpecTestCreator(create_test, TestSpec, os.path.join(SPEC_PATH, "legacy")) - test_creator.create_tests() - - if _HAVE_PYMONGOCRYPT: - globals().update( - generate_test_classes( - os.path.join(SPEC_PATH, "unified"), - module=__name__, - ) +class TestSpec(SpecRunner): + @classmethod + @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") + def _setup_class(cls): + super()._setup_class() + + def parse_auto_encrypt_opts(self, opts): + """Parse clientOptions.autoEncryptOpts.""" + opts = camel_to_snake_args(opts) + kms_providers = opts["kms_providers"] + if "aws" in kms_providers: + kms_providers["aws"] = AWS_CREDS + if not any(AWS_CREDS.values()): + self.skipTest("AWS environment credentials are not set") + if "awsTemporary" in kms_providers: + kms_providers["aws"] = AWS_TEMP_CREDS + del kms_providers["awsTemporary"] + if not any(AWS_TEMP_CREDS.values()): + self.skipTest("AWS Temp environment credentials are not set") + if "awsTemporaryNoSessionToken" in kms_providers: + kms_providers["aws"] = AWS_TEMP_NO_SESSION_CREDS + del kms_providers["awsTemporaryNoSessionToken"] + if not any(AWS_TEMP_NO_SESSION_CREDS.values()): + self.skipTest("AWS Temp environment credentials are not set") + if "azure" in kms_providers: + kms_providers["azure"] = AZURE_CREDS + if not any(AZURE_CREDS.values()): + self.skipTest("Azure environment credentials are not set") + if "gcp" in kms_providers: + kms_providers["gcp"] = GCP_CREDS + if not any(AZURE_CREDS.values()): + self.skipTest("GCP environment credentials are not set") + if "kmip" in kms_providers: + kms_providers["kmip"] = KMIP_CREDS + opts["kms_tls_options"] = KMS_TLS_OPTS + if "key_vault_namespace" not in opts: + opts["key_vault_namespace"] = "keyvault.datakeys" + if "extra_options" in opts: + opts.update(camel_to_snake_args(opts.pop("extra_options"))) + + opts = dict(opts) + return AutoEncryptionOpts(**opts) + + def parse_client_options(self, opts): + """Override clientOptions parsing to support autoEncryptOpts.""" + encrypt_opts = opts.pop("autoEncryptOpts", None) + if encrypt_opts: + opts["auto_encryption_opts"] = self.parse_auto_encrypt_opts(encrypt_opts) + + return super().parse_client_options(opts) + + def get_object_name(self, op): + """Default object is collection.""" + return op.get("object", "collection") + + def maybe_skip_scenario(self, test): + super().maybe_skip_scenario(test) + desc = test["description"].lower() + if ( + "timeoutms applied to listcollections to get collection schema" in desc + and sys.platform in ("win32", "darwin") + ): + self.skipTest("PYTHON-3706 flaky test on Windows/macOS") + if "type=symbol" in desc: + self.skipTest("PyMongo does not support the symbol type") + if "timeoutms applied to listcollections to get collection schema" in desc and not _IS_SYNC: + self.skipTest("PYTHON-4844 flaky test on async") + + def setup_scenario(self, scenario_def): + """Override a test's setup.""" + key_vault_data = scenario_def["key_vault_data"] + encrypted_fields = scenario_def["encrypted_fields"] + json_schema = scenario_def["json_schema"] + data = scenario_def["data"] + coll = client_context.client.get_database("keyvault", codec_options=OPTS)["datakeys"] + coll.delete_many({}) + if key_vault_data: + coll.insert_many(key_vault_data) + + db_name = self.get_scenario_db_name(scenario_def) + coll_name = self.get_scenario_coll_name(scenario_def) + db = client_context.client.get_database(db_name, codec_options=OPTS) + db.drop_collection(coll_name, encrypted_fields=encrypted_fields) + wc = WriteConcern(w="majority") + kwargs: Dict[str, Any] = {} + if json_schema: + kwargs["validator"] = {"$jsonSchema": json_schema} + kwargs["codec_options"] = OPTS + if not data: + kwargs["write_concern"] = wc + if encrypted_fields: + kwargs["encryptedFields"] = encrypted_fields + db.create_collection(coll_name, **kwargs) + coll = db[coll_name] + if data: + # Load data. + coll.with_options(write_concern=wc).insert_many(scenario_def["data"]) + + def allowable_errors(self, op): + """Override expected error classes.""" + errors = super().allowable_errors(op) + # An updateOne test expects encryption to error when no $ operator + # appears but pymongo raises a client side ValueError in this case. + if op["name"] == "updateOne": + errors += (ValueError,) + return errors + + +def create_test(scenario_def, test, name): + @client_context.require_test_commands + def run_scenario(self): + self.run_scenario(scenario_def, test) + + return run_scenario + + +test_creator = SpecTestCreator(create_test, TestSpec, os.path.join(SPEC_PATH, "legacy")) +test_creator.create_tests() + +if _HAVE_PYMONGOCRYPT: + globals().update( + generate_test_classes( + os.path.join(SPEC_PATH, "unified"), + module=__name__, ) + ) # Prose Tests ALL_KMS_PROVIDERS = { diff --git a/test/test_replica_set_reconfig.py b/test/test_replica_set_reconfig.py index 1dae0aea86..4c23d71b69 100644 --- a/test/test_replica_set_reconfig.py +++ b/test/test_replica_set_reconfig.py @@ -59,7 +59,8 @@ def test_client(self): with self.assertRaises(ServerSelectionTimeoutError): c.db.command("ping") - self.assertEqual(c.address, None) + with self.assertRaises(ServerSelectionTimeoutError): + _ = c.address # Client can still discover the primary node c.revive_host("a:1") diff --git a/test/test_server_selection_in_window.py b/test/test_server_selection_in_window.py index 7cab42cca2..05772fa385 100644 --- a/test/test_server_selection_in_window.py +++ b/test/test_server_selection_in_window.py @@ -21,11 +21,11 @@ from test.utils import ( CMAPListener, OvertCommandListener, - SpecTestCreator, get_pool, wait_until, ) from test.utils_selection_tests import create_topology +from test.utils_spec_runner import SpecTestCreator from pymongo.common import clean_node from pymongo.monitoring import ConnectionReadyEvent diff --git a/test/unified_format.py b/test/unified_format.py index 62211d3d25..13ab0af69b 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -18,41 +18,40 @@ """ from __future__ import annotations +import asyncio import binascii -import collections import copy -import datetime import functools import os import re import sys import time import traceback -import types -from collections import abc, defaultdict +from asyncio import iscoroutinefunction +from collections import defaultdict from test import ( IntegrationTest, client_context, client_knobs, unittest, ) -from test.helpers import ( - AWS_CREDS, - AWS_CREDS_2, - AZURE_CREDS, - CA_PEM, - CLIENT_PEM, - GCP_CREDS, - KMIP_CREDS, - LOCAL_MASTER_KEY, - client_knobs, +from test.unified_format_shared import ( + KMS_TLS_OPTS, + PLACEHOLDER_MAP, + SKIP_CSOT_TESTS, + EventListenerUtil, + MatchEvaluatorUtil, + coerce_result, + parse_bulk_write_error_result, + parse_bulk_write_result, + parse_client_bulk_write_error_result, + parse_collection_or_database_options, + with_metaclass, ) from test.utils import ( - CMAPListener, camel_to_snake, camel_to_snake_args, get_pool, - parse_collection_options, parse_spec_options, prepare_spec_arguments, snake_to_camel, @@ -60,14 +59,12 @@ ) from test.utils_spec_runner import SpecRunnerThread from test.version import Version -from typing import Any, Dict, List, Mapping, Optional, Union +from typing import Any, Dict, List, Mapping, Optional import pymongo -from bson import SON, Code, DBRef, Decimal128, Int64, MaxKey, MinKey, json_util -from bson.binary import Binary +from bson import SON, json_util from bson.codec_options import DEFAULT_CODEC_OPTIONS from bson.objectid import ObjectId -from bson.regex import RE_TYPE, Regex from gridfs import GridFSBucket, GridOut from pymongo import ASCENDING, CursorType, MongoClient, _csot from pymongo.encryption_options import _HAVE_PYMONGOCRYPT @@ -83,55 +80,14 @@ PyMongoError, ) from pymongo.monitoring import ( - _SENSITIVE_COMMANDS, - CommandFailedEvent, - CommandListener, CommandStartedEvent, - CommandSucceededEvent, - ConnectionCheckedInEvent, - ConnectionCheckedOutEvent, - ConnectionCheckOutFailedEvent, - ConnectionCheckOutStartedEvent, - ConnectionClosedEvent, - ConnectionCreatedEvent, - ConnectionReadyEvent, - PoolClearedEvent, - PoolClosedEvent, - PoolCreatedEvent, - PoolReadyEvent, - ServerClosedEvent, - ServerDescriptionChangedEvent, - ServerHeartbeatFailedEvent, - ServerHeartbeatListener, - ServerHeartbeatStartedEvent, - ServerHeartbeatSucceededEvent, - ServerListener, - ServerOpeningEvent, - TopologyClosedEvent, - TopologyDescriptionChangedEvent, - TopologyEvent, - TopologyListener, - TopologyOpenedEvent, - _CommandEvent, - _ConnectionEvent, - _PoolEvent, - _ServerEvent, - _ServerHeartbeatEvent, ) from pymongo.operations import ( - DeleteMany, - DeleteOne, - InsertOne, - ReplaceOne, SearchIndexModel, - UpdateMany, - UpdateOne, ) from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference -from pymongo.results import BulkWriteResult, ClientBulkWriteResult from pymongo.server_api import ServerApi -from pymongo.server_description import ServerDescription from pymongo.server_selectors import Selection, writable_server_selector from pymongo.server_type import SERVER_TYPE from pymongo.synchronous.change_stream import ChangeStream @@ -140,87 +96,21 @@ from pymongo.synchronous.command_cursor import CommandCursor from pymongo.synchronous.database import Database from pymongo.synchronous.encryption import ClientEncryption +from pymongo.synchronous.helpers import next from pymongo.topology_description import TopologyDescription from pymongo.typings import _Address from pymongo.write_concern import WriteConcern -SKIP_CSOT_TESTS = os.getenv("SKIP_CSOT_TESTS") - -JSON_OPTS = json_util.JSONOptions(tz_aware=False) +_IS_SYNC = True IS_INTERRUPTED = False -KMS_TLS_OPTS = { - "kmip": { - "tlsCAFile": CA_PEM, - "tlsCertificateKeyFile": CLIENT_PEM, - } -} - - -# Build up a placeholder maps. -PLACEHOLDER_MAP = {} -for provider_name, provider_data in [ - ("local", {"key": LOCAL_MASTER_KEY}), - ("local:name1", {"key": LOCAL_MASTER_KEY}), - ("aws", AWS_CREDS), - ("aws:name1", AWS_CREDS), - ("aws:name2", AWS_CREDS_2), - ("azure", AZURE_CREDS), - ("azure:name1", AZURE_CREDS), - ("gcp", GCP_CREDS), - ("gcp:name1", GCP_CREDS), - ("kmip", KMIP_CREDS), - ("kmip:name1", KMIP_CREDS), -]: - for key, value in provider_data.items(): - placeholder = f"/clientEncryptionOpts/kmsProviders/{provider_name}/{key}" - PLACEHOLDER_MAP[placeholder] = value - -OIDC_ENV = os.environ.get("OIDC_ENV", "test") -if OIDC_ENV == "test": - PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {"ENVIRONMENT": "test"} -elif OIDC_ENV == "azure": - PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = { - "ENVIRONMENT": "azure", - "TOKEN_RESOURCE": os.environ["AZUREOIDC_RESOURCE"], - } -elif OIDC_ENV == "gcp": - PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = { - "ENVIRONMENT": "gcp", - "TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"], - } - def interrupt_loop(): global IS_INTERRUPTED IS_INTERRUPTED = True -def with_metaclass(meta, *bases): - """Create a base class with a metaclass. - - Vendored from six: https://github.com/benjaminp/six/blob/master/six.py - """ - - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(type): - def __new__(cls, name, this_bases, d): - # __orig_bases__ is required by PEP 560. - resolved_bases = types.resolve_bases(bases) - if resolved_bases is not bases: - d["__orig_bases__"] = bases - return meta(name, resolved_bases, d) - - @classmethod - def __prepare__(cls, name, this_bases): - return meta.__prepare__(name, bases) - - return type.__new__(metaclass, "temporary_class", (), {}) - - def is_run_on_requirement_satisfied(requirement): topology_satisfied = True req_topologies = requirement.get("topologies") @@ -283,77 +173,6 @@ def is_run_on_requirement_satisfied(requirement): ) -def parse_collection_or_database_options(options): - return parse_collection_options(options) - - -def parse_bulk_write_result(result): - upserted_ids = {str(int_idx): result.upserted_ids[int_idx] for int_idx in result.upserted_ids} - return { - "deletedCount": result.deleted_count, - "insertedCount": result.inserted_count, - "matchedCount": result.matched_count, - "modifiedCount": result.modified_count, - "upsertedCount": result.upserted_count, - "upsertedIds": upserted_ids, - } - - -def parse_client_bulk_write_individual(op_type, result): - if op_type == "insert": - return {"insertedId": result.inserted_id} - if op_type == "update": - if result.upserted_id: - return { - "matchedCount": result.matched_count, - "modifiedCount": result.modified_count, - "upsertedId": result.upserted_id, - } - else: - return { - "matchedCount": result.matched_count, - "modifiedCount": result.modified_count, - } - if op_type == "delete": - return { - "deletedCount": result.deleted_count, - } - - -def parse_client_bulk_write_result(result): - insert_results, update_results, delete_results = {}, {}, {} - if result.has_verbose_results: - for idx, res in result.insert_results.items(): - insert_results[str(idx)] = parse_client_bulk_write_individual("insert", res) - for idx, res in result.update_results.items(): - update_results[str(idx)] = parse_client_bulk_write_individual("update", res) - for idx, res in result.delete_results.items(): - delete_results[str(idx)] = parse_client_bulk_write_individual("delete", res) - - return { - "deletedCount": result.deleted_count, - "insertedCount": result.inserted_count, - "matchedCount": result.matched_count, - "modifiedCount": result.modified_count, - "upsertedCount": result.upserted_count, - "insertResults": insert_results, - "updateResults": update_results, - "deleteResults": delete_results, - } - - -def parse_bulk_write_error_result(error): - write_result = BulkWriteResult(error.details, True) - return parse_bulk_write_result(write_result) - - -def parse_client_bulk_write_error_result(error): - write_result = error.partial_result - if not write_result: - return None - return parse_client_bulk_write_result(write_result) - - class NonLazyCursor: """A find cursor proxy that creates the remote cursor when initialized.""" @@ -361,7 +180,16 @@ def __init__(self, find_cursor, client): self.client = client self.find_cursor = find_cursor # Create the server side cursor. - self.first_result = next(find_cursor, None) + self.first_result = None + + @classmethod + def create(cls, find_cursor, client): + cursor = cls(find_cursor, client) + try: + cursor.first_result = next(cursor.find_cursor) + except StopIteration: + cursor.first_result = None + return cursor @property def alive(self): @@ -382,105 +210,6 @@ def close(self): self.client = None -class EventListenerUtil( - CMAPListener, CommandListener, ServerListener, ServerHeartbeatListener, TopologyListener -): - def __init__( - self, observe_events, ignore_commands, observe_sensitive_commands, store_events, entity_map - ): - self._event_types = {name.lower() for name in observe_events} - if observe_sensitive_commands: - self._observe_sensitive_commands = True - self._ignore_commands = set(ignore_commands) - else: - self._observe_sensitive_commands = False - self._ignore_commands = _SENSITIVE_COMMANDS | set(ignore_commands) - self._ignore_commands.add("configurefailpoint") - self._event_mapping = collections.defaultdict(list) - self.entity_map = entity_map - if store_events: - for i in store_events: - id = i["id"] - events = (i.lower() for i in i["events"]) - for i in events: - self._event_mapping[i].append(id) - self.entity_map[id] = [] - super().__init__() - - def get_events(self, event_type): - assert event_type in ("command", "cmap", "sdam", "all"), event_type - if event_type == "all": - return list(self.events) - if event_type == "command": - return [e for e in self.events if isinstance(e, _CommandEvent)] - if event_type == "cmap": - return [e for e in self.events if isinstance(e, (_ConnectionEvent, _PoolEvent))] - return [ - e - for e in self.events - if isinstance(e, (_ServerEvent, TopologyEvent, _ServerHeartbeatEvent)) - ] - - def add_event(self, event): - event_name = type(event).__name__.lower() - if event_name in self._event_types: - super().add_event(event) - for id in self._event_mapping[event_name]: - self.entity_map[id].append( - { - "name": type(event).__name__, - "observedAt": time.time(), - "description": repr(event), - } - ) - - def _command_event(self, event): - if event.command_name.lower() not in self._ignore_commands: - self.add_event(event) - - def started(self, event): - if isinstance(event, CommandStartedEvent): - if event.command == {}: - # Command is redacted. Observe only if flag is set. - if self._observe_sensitive_commands: - self._command_event(event) - else: - self._command_event(event) - else: - self.add_event(event) - - def succeeded(self, event): - if isinstance(event, CommandSucceededEvent): - if event.reply == {}: - # Command is redacted. Observe only if flag is set. - if self._observe_sensitive_commands: - self._command_event(event) - else: - self._command_event(event) - else: - self.add_event(event) - - def failed(self, event): - if isinstance(event, CommandFailedEvent): - self._command_event(event) - else: - self.add_event(event) - - def opened(self, event: Union[ServerOpeningEvent, TopologyOpenedEvent]) -> None: - self.add_event(event) - - def description_changed( - self, event: Union[ServerDescriptionChangedEvent, TopologyDescriptionChangedEvent] - ) -> None: - self.add_event(event) - - def topology_changed(self, event: TopologyDescriptionChangedEvent) -> None: - self.add_event(event) - - def closed(self, event: Union[ServerClosedEvent, TopologyClosedEvent]) -> None: - self.add_event(event) - - class EntityMapUtil: """Utility class that implements an entity map as per the unified test format specification. @@ -692,353 +421,12 @@ def get_lsid_for_session(self, session_name): def advance_cluster_times(self) -> None: """Manually synchronize entities when desired""" if not self._cluster_time: - self._cluster_time = self.test.client.admin.command("ping").get("$clusterTime") + self._cluster_time = (self.test.client.admin.command("ping")).get("$clusterTime") for entity in self._entities.values(): if isinstance(entity, ClientSession) and self._cluster_time: entity.advance_cluster_time(self._cluster_time) -binary_types = (Binary, bytes) -long_types = (Int64,) -unicode_type = str - - -BSON_TYPE_ALIAS_MAP = { - # https://mongodb.com/docs/manual/reference/operator/query/type/ - # https://pymongo.readthedocs.io/en/stable/api/bson/index.html - "double": (float,), - "string": (str,), - "object": (abc.Mapping,), - "array": (abc.MutableSequence,), - "binData": binary_types, - "undefined": (type(None),), - "objectId": (ObjectId,), - "bool": (bool,), - "date": (datetime.datetime,), - "null": (type(None),), - "regex": (Regex, RE_TYPE), - "dbPointer": (DBRef,), - "javascript": (unicode_type, Code), - "symbol": (unicode_type,), - "javascriptWithScope": (unicode_type, Code), - "int": (int,), - "long": (Int64,), - "decimal": (Decimal128,), - "maxKey": (MaxKey,), - "minKey": (MinKey,), -} - - -class MatchEvaluatorUtil: - """Utility class that implements methods for evaluating matches as per - the unified test format specification. - """ - - def __init__(self, test_class): - self.test = test_class - - def _operation_exists(self, spec, actual, key_to_compare): - if spec is True: - if key_to_compare is None: - assert actual is not None - else: - self.test.assertIn(key_to_compare, actual) - elif spec is False: - if key_to_compare is None: - assert actual is None - else: - self.test.assertNotIn(key_to_compare, actual) - else: - self.test.fail(f"Expected boolean value for $$exists operator, got {spec}") - - def __type_alias_to_type(self, alias): - if alias not in BSON_TYPE_ALIAS_MAP: - self.test.fail(f"Unrecognized BSON type alias {alias}") - return BSON_TYPE_ALIAS_MAP[alias] - - def _operation_type(self, spec, actual, key_to_compare): - if isinstance(spec, abc.MutableSequence): - permissible_types = tuple( - [t for alias in spec for t in self.__type_alias_to_type(alias)] - ) - else: - permissible_types = self.__type_alias_to_type(spec) - value = actual[key_to_compare] if key_to_compare else actual - self.test.assertIsInstance(value, permissible_types) - - def _operation_matchesEntity(self, spec, actual, key_to_compare): - expected_entity = self.test.entity_map[spec] - self.test.assertEqual(expected_entity, actual[key_to_compare]) - - def _operation_matchesHexBytes(self, spec, actual, key_to_compare): - expected = binascii.unhexlify(spec) - value = actual[key_to_compare] if key_to_compare else actual - self.test.assertEqual(value, expected) - - def _operation_unsetOrMatches(self, spec, actual, key_to_compare): - if key_to_compare is None and not actual: - # top-level document can be None when unset - return - - if key_to_compare not in actual: - # we add a dummy value for the compared key to pass map size check - actual[key_to_compare] = "dummyValue" - return - self.match_result(spec, actual[key_to_compare], in_recursive_call=True) - - def _operation_sessionLsid(self, spec, actual, key_to_compare): - expected_lsid = self.test.entity_map.get_lsid_for_session(spec) - self.test.assertEqual(expected_lsid, actual[key_to_compare]) - - def _operation_lte(self, spec, actual, key_to_compare): - if key_to_compare not in actual: - self.test.fail(f"Actual command is missing the {key_to_compare} field: {spec}") - self.test.assertLessEqual(actual[key_to_compare], spec) - - def _operation_matchAsDocument(self, spec, actual, key_to_compare): - self._match_document(spec, json_util.loads(actual[key_to_compare]), False) - - def _operation_matchAsRoot(self, spec, actual, key_to_compare): - self._match_document(spec, actual, True) - - def _evaluate_special_operation(self, opname, spec, actual, key_to_compare): - method_name = "_operation_{}".format(opname.strip("$")) - try: - method = getattr(self, method_name) - except AttributeError: - self.test.fail(f"Unsupported special matching operator {opname}") - else: - method(spec, actual, key_to_compare) - - def _evaluate_if_special_operation(self, expectation, actual, key_to_compare=None): - """Returns True if a special operation is evaluated, False - otherwise. If the ``expectation`` map contains a single key, - value pair we check it for a special operation. - If given, ``key_to_compare`` is assumed to be the key in - ``expectation`` whose corresponding value needs to be - evaluated for a possible special operation. ``key_to_compare`` - is ignored when ``expectation`` has only one key. - """ - if not isinstance(expectation, abc.Mapping): - return False - - is_special_op, opname, spec = False, False, False - - if key_to_compare is not None: - if key_to_compare.startswith("$$"): - is_special_op = True - opname = key_to_compare - spec = expectation[key_to_compare] - key_to_compare = None - else: - nested = expectation[key_to_compare] - if isinstance(nested, abc.Mapping) and len(nested) == 1: - opname, spec = next(iter(nested.items())) - if opname.startswith("$$"): - is_special_op = True - elif len(expectation) == 1: - opname, spec = next(iter(expectation.items())) - if opname.startswith("$$"): - is_special_op = True - key_to_compare = None - - if is_special_op: - self._evaluate_special_operation( - opname=opname, spec=spec, actual=actual, key_to_compare=key_to_compare - ) - return True - - return False - - def _match_document(self, expectation, actual, is_root, test=False): - if self._evaluate_if_special_operation(expectation, actual): - return - - self.test.assertIsInstance(actual, abc.Mapping) - for key, value in expectation.items(): - if self._evaluate_if_special_operation(expectation, actual, key): - continue - - self.test.assertIn(key, actual) - if not self.match_result(value, actual[key], in_recursive_call=True, test=test): - return False - - if not is_root: - expected_keys = set(expectation.keys()) - for key, value in expectation.items(): - if value == {"$$exists": False}: - expected_keys.remove(key) - if test: - self.test.assertEqual(expected_keys, set(actual.keys())) - else: - return set(expected_keys).issubset(set(actual.keys())) - return True - - def match_result(self, expectation, actual, in_recursive_call=False, test=True): - if isinstance(expectation, abc.Mapping): - return self._match_document( - expectation, actual, is_root=not in_recursive_call, test=test - ) - - if isinstance(expectation, abc.MutableSequence): - self.test.assertIsInstance(actual, abc.MutableSequence) - for e, a in zip(expectation, actual): - if isinstance(e, abc.Mapping): - self._match_document(e, a, is_root=not in_recursive_call, test=test) - else: - self.match_result(e, a, in_recursive_call=True, test=test) - return None - - # account for flexible numerics in element-wise comparison - if isinstance(expectation, int) or isinstance(expectation, float): - if test: - self.test.assertEqual(expectation, actual) - else: - return expectation == actual - return None - else: - if test: - self.test.assertIsInstance(actual, type(expectation)) - self.test.assertEqual(expectation, actual) - else: - return isinstance(actual, type(expectation)) and expectation == actual - return None - - def match_server_description(self, actual: ServerDescription, spec: dict) -> None: - for field, expected in spec.items(): - field = camel_to_snake(field) - if field == "type": - field = "server_type_name" - self.test.assertEqual(getattr(actual, field), expected) - - def match_topology_description(self, actual: TopologyDescription, spec: dict) -> None: - for field, expected in spec.items(): - field = camel_to_snake(field) - if field == "type": - field = "topology_type_name" - self.test.assertEqual(getattr(actual, field), expected) - - def match_event_fields(self, actual: Any, spec: dict) -> None: - for field, expected in spec.items(): - if field == "command" and isinstance(actual, CommandStartedEvent): - command = spec["command"] - if command: - self.match_result(command, actual.command) - continue - if field == "reply" and isinstance(actual, CommandSucceededEvent): - reply = spec["reply"] - if reply: - self.match_result(reply, actual.reply) - continue - if field == "hasServiceId": - if spec["hasServiceId"]: - self.test.assertIsNotNone(actual.service_id) - self.test.assertIsInstance(actual.service_id, ObjectId) - else: - self.test.assertIsNone(actual.service_id) - continue - if field == "hasServerConnectionId": - if spec["hasServerConnectionId"]: - self.test.assertIsNotNone(actual.server_connection_id) - self.test.assertIsInstance(actual.server_connection_id, int) - else: - self.test.assertIsNone(actual.server_connection_id) - continue - if field in ("previousDescription", "newDescription"): - if isinstance(actual, ServerDescriptionChangedEvent): - self.match_server_description( - getattr(actual, camel_to_snake(field)), spec[field] - ) - continue - if isinstance(actual, TopologyDescriptionChangedEvent): - self.match_topology_description( - getattr(actual, camel_to_snake(field)), spec[field] - ) - continue - - if field == "interruptInUseConnections": - field = "interrupt_connections" - else: - field = camel_to_snake(field) - self.test.assertEqual(getattr(actual, field), expected) - - def match_event(self, expectation, actual): - name, spec = next(iter(expectation.items())) - if name == "commandStartedEvent": - self.test.assertIsInstance(actual, CommandStartedEvent) - elif name == "commandSucceededEvent": - self.test.assertIsInstance(actual, CommandSucceededEvent) - elif name == "commandFailedEvent": - self.test.assertIsInstance(actual, CommandFailedEvent) - elif name == "poolCreatedEvent": - self.test.assertIsInstance(actual, PoolCreatedEvent) - elif name == "poolReadyEvent": - self.test.assertIsInstance(actual, PoolReadyEvent) - elif name == "poolClearedEvent": - self.test.assertIsInstance(actual, PoolClearedEvent) - self.test.assertIsInstance(actual.interrupt_connections, bool) - elif name == "poolClosedEvent": - self.test.assertIsInstance(actual, PoolClosedEvent) - elif name == "connectionCreatedEvent": - self.test.assertIsInstance(actual, ConnectionCreatedEvent) - elif name == "connectionReadyEvent": - self.test.assertIsInstance(actual, ConnectionReadyEvent) - elif name == "connectionClosedEvent": - self.test.assertIsInstance(actual, ConnectionClosedEvent) - elif name == "connectionCheckOutStartedEvent": - self.test.assertIsInstance(actual, ConnectionCheckOutStartedEvent) - elif name == "connectionCheckOutFailedEvent": - self.test.assertIsInstance(actual, ConnectionCheckOutFailedEvent) - elif name == "connectionCheckedOutEvent": - self.test.assertIsInstance(actual, ConnectionCheckedOutEvent) - elif name == "connectionCheckedInEvent": - self.test.assertIsInstance(actual, ConnectionCheckedInEvent) - elif name == "serverDescriptionChangedEvent": - self.test.assertIsInstance(actual, ServerDescriptionChangedEvent) - elif name == "serverHeartbeatStartedEvent": - self.test.assertIsInstance(actual, ServerHeartbeatStartedEvent) - elif name == "serverHeartbeatSucceededEvent": - self.test.assertIsInstance(actual, ServerHeartbeatSucceededEvent) - elif name == "serverHeartbeatFailedEvent": - self.test.assertIsInstance(actual, ServerHeartbeatFailedEvent) - elif name == "topologyDescriptionChangedEvent": - self.test.assertIsInstance(actual, TopologyDescriptionChangedEvent) - elif name == "topologyOpeningEvent": - self.test.assertIsInstance(actual, TopologyOpenedEvent) - elif name == "topologyClosedEvent": - self.test.assertIsInstance(actual, TopologyClosedEvent) - else: - raise Exception(f"Unsupported event type {name}") - - self.match_event_fields(actual, spec) - - -def coerce_result(opname, result): - """Convert a pymongo result into the spec's result format.""" - if hasattr(result, "acknowledged") and not result.acknowledged: - return {"acknowledged": False} - if opname == "bulkWrite": - return parse_bulk_write_result(result) - if opname == "clientBulkWrite": - return parse_client_bulk_write_result(result) - if opname == "insertOne": - return {"insertedId": result.inserted_id} - if opname == "insertMany": - return dict(enumerate(result.inserted_ids)) - if opname in ("deleteOne", "deleteMany"): - return {"deletedCount": result.deleted_count} - if opname in ("updateOne", "updateMany", "replaceOne"): - value = { - "matchedCount": result.matched_count, - "modifiedCount": result.modified_count, - "upsertedCount": 0 if result.upserted_id is None else 1, - } - if result.upserted_id is not None: - value["upsertedId"] = result.upserted_id - return value - return result - - class UnifiedSpecTestMixinV1(IntegrationTest): """Mixin class to run test cases from test specification files. @@ -1090,9 +478,9 @@ def insert_initial_data(self, initial_data): db.create_collection(coll_name, write_concern=wc, **opts) @classmethod - def setUpClass(cls): + def _setup_class(cls): # super call creates internal client cls.client - super().setUpClass() + super()._setup_class() # process file-level runOnRequirements run_on_spec = cls.TEST_SPEC.get("runOnRequirements", []) if not cls.should_run_on(run_on_spec): @@ -1125,11 +513,11 @@ def setUpClass(cls): cls.knobs.enable() @classmethod - def tearDownClass(cls): + def _tearDown_class(cls): cls.knobs.disable() for client in cls.mongos_clients: client.close() - super().tearDownClass() + super()._tearDown_class() def setUp(self): super().setUp() @@ -1391,7 +779,7 @@ def _databaseOperation_createCollection(self, target, *args, **kwargs): def __entityOperation_aggregate(self, target, *args, **kwargs): self.__raise_if_unsupported("aggregate", target, Database, Collection) - return list(target.aggregate(*args, **kwargs)) + return (target.aggregate(*args, **kwargs)).to_list() def _databaseOperation_aggregate(self, target, *args, **kwargs): return self.__entityOperation_aggregate(target, *args, **kwargs) @@ -1402,13 +790,13 @@ def _collectionOperation_aggregate(self, target, *args, **kwargs): def _collectionOperation_find(self, target, *args, **kwargs): self.__raise_if_unsupported("find", target, Collection) find_cursor = target.find(*args, **kwargs) - return list(find_cursor) + return find_cursor.to_list() def _collectionOperation_createFindCursor(self, target, *args, **kwargs): self.__raise_if_unsupported("find", target, Collection) if "filter" not in kwargs: self.fail('createFindCursor requires a "filter" argument') - cursor = NonLazyCursor(target.find(*args, **kwargs), target.database.client) + cursor = NonLazyCursor.create(target.find(*args, **kwargs), target.database.client) self.addCleanup(cursor.close) return cursor @@ -1418,7 +806,7 @@ def _collectionOperation_count(self, target, *args, **kwargs): def _collectionOperation_listIndexes(self, target, *args, **kwargs): if "batch_size" in kwargs: self.skipTest("PyMongo does not support batch_size for list_indexes") - return list(target.list_indexes(*args, **kwargs)) + return (target.list_indexes(*args, **kwargs)).to_list() def _collectionOperation_listIndexNames(self, target, *args, **kwargs): self.skipTest("PyMongo does not support list_index_names") @@ -1430,7 +818,7 @@ def _collectionOperation_createSearchIndexes(self, target, *args, **kwargs): def _collectionOperation_listSearchIndexes(self, target, *args, **kwargs): name = kwargs.get("name") agg_kwargs = kwargs.get("aggregation_options", dict()) - return list(target.list_search_indexes(name, **agg_kwargs)) + return (target.list_search_indexes(name, **agg_kwargs)).to_list() def _sessionOperation_withTransaction(self, target, *args, **kwargs): if client_context.storage_engine == "mmapv1": @@ -1470,7 +858,7 @@ def _clientEncryptionOperation_createDataKey(self, target, *args, **kwargs): return target.create_data_key(*args, **kwargs) def _clientEncryptionOperation_getKeys(self, target, *args, **kwargs): - return list(target.get_keys(*args, **kwargs)) + return (target.get_keys(*args, **kwargs)).to_list() def _clientEncryptionOperation_deleteKey(self, target, *args, **kwargs): result = target.delete_key(*args, **kwargs) @@ -1516,7 +904,7 @@ def _bucketOperation_uploadWithId(self, target: GridFSBucket, *args: Any, **kwar def _bucketOperation_find( self, target: GridFSBucket, *args: Any, **kwargs: Any ) -> List[GridOut]: - return list(target.find(*args, **kwargs)) + return target.find(*args, **kwargs).to_list() def run_entity_operation(self, spec): target = self.entity_map[spec["object"]] @@ -1849,7 +1237,10 @@ def run_special_operation(self, spec): except AttributeError: self.fail(f"Unsupported special test operation {opname}") else: - method(spec["arguments"]) + if iscoroutinefunction(method): + method(spec["arguments"]) + else: + method(spec["arguments"]) def run_operations(self, spec): for op in spec: @@ -1985,7 +1376,7 @@ def verify_outcome(self, spec): if expected_documents: sorted_expected_documents = sorted(expected_documents, key=lambda doc: doc["_id"]) - actual_documents = list(coll.find({}, sort=[("_id", ASCENDING)])) + actual_documents = coll.find({}, sort=[("_id", ASCENDING)]).to_list() self.assertListEqual(sorted_expected_documents, actual_documents) def run_scenario(self, spec, uri=None): @@ -2040,7 +1431,7 @@ def _run_scenario(self, spec, uri=None): # process initialData if "initialData" in self.TEST_SPEC: self.insert_initial_data(self.TEST_SPEC["initialData"]) - self._cluster_time = self.client.admin.command("ping").get("$clusterTime") + self._cluster_time = (self.client.admin.command("ping")).get("$clusterTime") self.entity_map.advance_cluster_times() if "expectLogMessages" in spec: diff --git a/test/unified_format_shared.py b/test/unified_format_shared.py new file mode 100644 index 0000000000..f1b908a7a6 --- /dev/null +++ b/test/unified_format_shared.py @@ -0,0 +1,674 @@ +# Copyright 2024-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. + +"""Shared utility functions and constants for the unified test format runner. + +https://github.com/mongodb/specifications/blob/master/source/unified-test-format/unified-test-format.rst +""" +from __future__ import annotations + +import binascii +import collections +import datetime +import os +import time +import types +from collections import abc +from test.helpers import ( + AWS_CREDS, + AWS_CREDS_2, + AZURE_CREDS, + CA_PEM, + CLIENT_PEM, + GCP_CREDS, + KMIP_CREDS, + LOCAL_MASTER_KEY, +) +from test.utils import CMAPListener, camel_to_snake, parse_collection_options +from typing import Any, Union + +from bson import ( + RE_TYPE, + Binary, + Code, + DBRef, + Decimal128, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + json_util, +) +from pymongo.monitoring import ( + _SENSITIVE_COMMANDS, + CommandFailedEvent, + CommandListener, + CommandStartedEvent, + CommandSucceededEvent, + ConnectionCheckedInEvent, + ConnectionCheckedOutEvent, + ConnectionCheckOutFailedEvent, + ConnectionCheckOutStartedEvent, + ConnectionClosedEvent, + ConnectionCreatedEvent, + ConnectionReadyEvent, + PoolClearedEvent, + PoolClosedEvent, + PoolCreatedEvent, + PoolReadyEvent, + ServerClosedEvent, + ServerDescriptionChangedEvent, + ServerHeartbeatFailedEvent, + ServerHeartbeatListener, + ServerHeartbeatStartedEvent, + ServerHeartbeatSucceededEvent, + ServerListener, + ServerOpeningEvent, + TopologyClosedEvent, + TopologyDescriptionChangedEvent, + TopologyEvent, + TopologyListener, + TopologyOpenedEvent, + _CommandEvent, + _ConnectionEvent, + _PoolEvent, + _ServerEvent, + _ServerHeartbeatEvent, +) +from pymongo.results import BulkWriteResult +from pymongo.server_description import ServerDescription +from pymongo.topology_description import TopologyDescription + +SKIP_CSOT_TESTS = os.getenv("SKIP_CSOT_TESTS") + +JSON_OPTS = json_util.JSONOptions(tz_aware=False) + +IS_INTERRUPTED = False + +KMS_TLS_OPTS = { + "kmip": { + "tlsCAFile": CA_PEM, + "tlsCertificateKeyFile": CLIENT_PEM, + } +} + + +# Build up a placeholder maps. +PLACEHOLDER_MAP = {} +for provider_name, provider_data in [ + ("local", {"key": LOCAL_MASTER_KEY}), + ("local:name1", {"key": LOCAL_MASTER_KEY}), + ("aws", AWS_CREDS), + ("aws:name1", AWS_CREDS), + ("aws:name2", AWS_CREDS_2), + ("azure", AZURE_CREDS), + ("azure:name1", AZURE_CREDS), + ("gcp", GCP_CREDS), + ("gcp:name1", GCP_CREDS), + ("kmip", KMIP_CREDS), + ("kmip:name1", KMIP_CREDS), +]: + for key, value in provider_data.items(): + placeholder = f"/clientEncryptionOpts/kmsProviders/{provider_name}/{key}" + PLACEHOLDER_MAP[placeholder] = value + +OIDC_ENV = os.environ.get("OIDC_ENV", "test") +if OIDC_ENV == "test": + PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {"ENVIRONMENT": "test"} +elif OIDC_ENV == "azure": + PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": os.environ["AZUREOIDC_RESOURCE"], + } +elif OIDC_ENV == "gcp": + PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = { + "ENVIRONMENT": "gcp", + "TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"], + } + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass. + + Vendored from six: https://github.com/benjaminp/six/blob/master/six.py + """ + + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(type): + def __new__(cls, name, this_bases, d): + # __orig_bases__ is required by PEP 560. + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d["__orig_bases__"] = bases + return meta(name, resolved_bases, d) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + + return type.__new__(metaclass, "temporary_class", (), {}) + + +def parse_collection_or_database_options(options): + return parse_collection_options(options) + + +def parse_bulk_write_result(result): + upserted_ids = {str(int_idx): result.upserted_ids[int_idx] for int_idx in result.upserted_ids} + return { + "deletedCount": result.deleted_count, + "insertedCount": result.inserted_count, + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count, + "upsertedCount": result.upserted_count, + "upsertedIds": upserted_ids, + } + + +def parse_client_bulk_write_individual(op_type, result): + if op_type == "insert": + return {"insertedId": result.inserted_id} + if op_type == "update": + if result.upserted_id: + return { + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count, + "upsertedId": result.upserted_id, + } + else: + return { + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count, + } + if op_type == "delete": + return { + "deletedCount": result.deleted_count, + } + + +def parse_client_bulk_write_result(result): + insert_results, update_results, delete_results = {}, {}, {} + if result.has_verbose_results: + for idx, res in result.insert_results.items(): + insert_results[str(idx)] = parse_client_bulk_write_individual("insert", res) + for idx, res in result.update_results.items(): + update_results[str(idx)] = parse_client_bulk_write_individual("update", res) + for idx, res in result.delete_results.items(): + delete_results[str(idx)] = parse_client_bulk_write_individual("delete", res) + + return { + "deletedCount": result.deleted_count, + "insertedCount": result.inserted_count, + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count, + "upsertedCount": result.upserted_count, + "insertResults": insert_results, + "updateResults": update_results, + "deleteResults": delete_results, + } + + +def parse_bulk_write_error_result(error): + write_result = BulkWriteResult(error.details, True) + return parse_bulk_write_result(write_result) + + +def parse_client_bulk_write_error_result(error): + write_result = error.partial_result + if not write_result: + return None + return parse_client_bulk_write_result(write_result) + + +class EventListenerUtil( + CMAPListener, CommandListener, ServerListener, ServerHeartbeatListener, TopologyListener +): + def __init__( + self, observe_events, ignore_commands, observe_sensitive_commands, store_events, entity_map + ): + self._event_types = {name.lower() for name in observe_events} + if observe_sensitive_commands: + self._observe_sensitive_commands = True + self._ignore_commands = set(ignore_commands) + else: + self._observe_sensitive_commands = False + self._ignore_commands = _SENSITIVE_COMMANDS | set(ignore_commands) + self._ignore_commands.add("configurefailpoint") + self._event_mapping = collections.defaultdict(list) + self.entity_map = entity_map + if store_events: + for i in store_events: + id = i["id"] + events = (i.lower() for i in i["events"]) + for i in events: + self._event_mapping[i].append(id) + self.entity_map[id] = [] + super().__init__() + + def get_events(self, event_type): + assert event_type in ("command", "cmap", "sdam", "all"), event_type + if event_type == "all": + return list(self.events) + if event_type == "command": + return [e for e in self.events if isinstance(e, _CommandEvent)] + if event_type == "cmap": + return [e for e in self.events if isinstance(e, (_ConnectionEvent, _PoolEvent))] + return [ + e + for e in self.events + if isinstance(e, (_ServerEvent, TopologyEvent, _ServerHeartbeatEvent)) + ] + + def add_event(self, event): + event_name = type(event).__name__.lower() + if event_name in self._event_types: + super().add_event(event) + for id in self._event_mapping[event_name]: + self.entity_map[id].append( + { + "name": type(event).__name__, + "observedAt": time.time(), + "description": repr(event), + } + ) + + def _command_event(self, event): + if event.command_name.lower() not in self._ignore_commands: + self.add_event(event) + + def started(self, event): + if isinstance(event, CommandStartedEvent): + if event.command == {}: + # Command is redacted. Observe only if flag is set. + if self._observe_sensitive_commands: + self._command_event(event) + else: + self._command_event(event) + else: + self.add_event(event) + + def succeeded(self, event): + if isinstance(event, CommandSucceededEvent): + if event.reply == {}: + # Command is redacted. Observe only if flag is set. + if self._observe_sensitive_commands: + self._command_event(event) + else: + self._command_event(event) + else: + self.add_event(event) + + def failed(self, event): + if isinstance(event, CommandFailedEvent): + self._command_event(event) + else: + self.add_event(event) + + def opened(self, event: Union[ServerOpeningEvent, TopologyOpenedEvent]) -> None: + self.add_event(event) + + def description_changed( + self, event: Union[ServerDescriptionChangedEvent, TopologyDescriptionChangedEvent] + ) -> None: + self.add_event(event) + + def topology_changed(self, event: TopologyDescriptionChangedEvent) -> None: + self.add_event(event) + + def closed(self, event: Union[ServerClosedEvent, TopologyClosedEvent]) -> None: + self.add_event(event) + + +binary_types = (Binary, bytes) +long_types = (Int64,) +unicode_type = str + + +BSON_TYPE_ALIAS_MAP = { + # https://mongodb.com/docs/manual/reference/operator/query/type/ + # https://pymongo.readthedocs.io/en/stable/api/bson/index.html + "double": (float,), + "string": (str,), + "object": (abc.Mapping,), + "array": (abc.MutableSequence,), + "binData": binary_types, + "undefined": (type(None),), + "objectId": (ObjectId,), + "bool": (bool,), + "date": (datetime.datetime,), + "null": (type(None),), + "regex": (Regex, RE_TYPE), + "dbPointer": (DBRef,), + "javascript": (unicode_type, Code), + "symbol": (unicode_type,), + "javascriptWithScope": (unicode_type, Code), + "int": (int,), + "long": (Int64,), + "decimal": (Decimal128,), + "maxKey": (MaxKey,), + "minKey": (MinKey,), +} + + +class MatchEvaluatorUtil: + """Utility class that implements methods for evaluating matches as per + the unified test format specification. + """ + + def __init__(self, test_class): + self.test = test_class + + def _operation_exists(self, spec, actual, key_to_compare): + if spec is True: + if key_to_compare is None: + assert actual is not None + else: + self.test.assertIn(key_to_compare, actual) + elif spec is False: + if key_to_compare is None: + assert actual is None + else: + self.test.assertNotIn(key_to_compare, actual) + else: + self.test.fail(f"Expected boolean value for $$exists operator, got {spec}") + + def __type_alias_to_type(self, alias): + if alias not in BSON_TYPE_ALIAS_MAP: + self.test.fail(f"Unrecognized BSON type alias {alias}") + return BSON_TYPE_ALIAS_MAP[alias] + + def _operation_type(self, spec, actual, key_to_compare): + if isinstance(spec, abc.MutableSequence): + permissible_types = tuple( + [t for alias in spec for t in self.__type_alias_to_type(alias)] + ) + else: + permissible_types = self.__type_alias_to_type(spec) + value = actual[key_to_compare] if key_to_compare else actual + self.test.assertIsInstance(value, permissible_types) + + def _operation_matchesEntity(self, spec, actual, key_to_compare): + expected_entity = self.test.entity_map[spec] + self.test.assertEqual(expected_entity, actual[key_to_compare]) + + def _operation_matchesHexBytes(self, spec, actual, key_to_compare): + expected = binascii.unhexlify(spec) + value = actual[key_to_compare] if key_to_compare else actual + self.test.assertEqual(value, expected) + + def _operation_unsetOrMatches(self, spec, actual, key_to_compare): + if key_to_compare is None and not actual: + # top-level document can be None when unset + return + + if key_to_compare not in actual: + # we add a dummy value for the compared key to pass map size check + actual[key_to_compare] = "dummyValue" + return + self.match_result(spec, actual[key_to_compare], in_recursive_call=True) + + def _operation_sessionLsid(self, spec, actual, key_to_compare): + expected_lsid = self.test.entity_map.get_lsid_for_session(spec) + self.test.assertEqual(expected_lsid, actual[key_to_compare]) + + def _operation_lte(self, spec, actual, key_to_compare): + if key_to_compare not in actual: + self.test.fail(f"Actual command is missing the {key_to_compare} field: {spec}") + self.test.assertLessEqual(actual[key_to_compare], spec) + + def _operation_matchAsDocument(self, spec, actual, key_to_compare): + self._match_document(spec, json_util.loads(actual[key_to_compare]), False) + + def _operation_matchAsRoot(self, spec, actual, key_to_compare): + self._match_document(spec, actual, True) + + def _evaluate_special_operation(self, opname, spec, actual, key_to_compare): + method_name = "_operation_{}".format(opname.strip("$")) + try: + method = getattr(self, method_name) + except AttributeError: + self.test.fail(f"Unsupported special matching operator {opname}") + else: + method(spec, actual, key_to_compare) + + def _evaluate_if_special_operation(self, expectation, actual, key_to_compare=None): + """Returns True if a special operation is evaluated, False + otherwise. If the ``expectation`` map contains a single key, + value pair we check it for a special operation. + If given, ``key_to_compare`` is assumed to be the key in + ``expectation`` whose corresponding value needs to be + evaluated for a possible special operation. ``key_to_compare`` + is ignored when ``expectation`` has only one key. + """ + if not isinstance(expectation, abc.Mapping): + return False + + is_special_op, opname, spec = False, False, False + + if key_to_compare is not None: + if key_to_compare.startswith("$$"): + is_special_op = True + opname = key_to_compare + spec = expectation[key_to_compare] + key_to_compare = None + else: + nested = expectation[key_to_compare] + if isinstance(nested, abc.Mapping) and len(nested) == 1: + opname, spec = next(iter(nested.items())) + if opname.startswith("$$"): + is_special_op = True + elif len(expectation) == 1: + opname, spec = next(iter(expectation.items())) + if opname.startswith("$$"): + is_special_op = True + key_to_compare = None + + if is_special_op: + self._evaluate_special_operation( + opname=opname, spec=spec, actual=actual, key_to_compare=key_to_compare + ) + return True + + return False + + def _match_document(self, expectation, actual, is_root, test=False): + if self._evaluate_if_special_operation(expectation, actual): + return + + self.test.assertIsInstance(actual, abc.Mapping) + for key, value in expectation.items(): + if self._evaluate_if_special_operation(expectation, actual, key): + continue + + self.test.assertIn(key, actual) + if not self.match_result(value, actual[key], in_recursive_call=True, test=test): + return False + + if not is_root: + expected_keys = set(expectation.keys()) + for key, value in expectation.items(): + if value == {"$$exists": False}: + expected_keys.remove(key) + if test: + self.test.assertEqual(expected_keys, set(actual.keys())) + else: + return set(expected_keys).issubset(set(actual.keys())) + return True + + def match_result(self, expectation, actual, in_recursive_call=False, test=True): + if isinstance(expectation, abc.Mapping): + return self._match_document( + expectation, actual, is_root=not in_recursive_call, test=test + ) + + if isinstance(expectation, abc.MutableSequence): + self.test.assertIsInstance(actual, abc.MutableSequence) + for e, a in zip(expectation, actual): + if isinstance(e, abc.Mapping): + self._match_document(e, a, is_root=not in_recursive_call, test=test) + else: + self.match_result(e, a, in_recursive_call=True, test=test) + return None + + # account for flexible numerics in element-wise comparison + if isinstance(expectation, int) or isinstance(expectation, float): + if test: + self.test.assertEqual(expectation, actual) + else: + return expectation == actual + return None + else: + if test: + self.test.assertIsInstance(actual, type(expectation)) + self.test.assertEqual(expectation, actual) + else: + return isinstance(actual, type(expectation)) and expectation == actual + return None + + def match_server_description(self, actual: ServerDescription, spec: dict) -> None: + for field, expected in spec.items(): + field = camel_to_snake(field) + if field == "type": + field = "server_type_name" + self.test.assertEqual(getattr(actual, field), expected) + + def match_topology_description(self, actual: TopologyDescription, spec: dict) -> None: + for field, expected in spec.items(): + field = camel_to_snake(field) + if field == "type": + field = "topology_type_name" + self.test.assertEqual(getattr(actual, field), expected) + + def match_event_fields(self, actual: Any, spec: dict) -> None: + for field, expected in spec.items(): + if field == "command" and isinstance(actual, CommandStartedEvent): + command = spec["command"] + if command: + self.match_result(command, actual.command) + continue + if field == "reply" and isinstance(actual, CommandSucceededEvent): + reply = spec["reply"] + if reply: + self.match_result(reply, actual.reply) + continue + if field == "hasServiceId": + if spec["hasServiceId"]: + self.test.assertIsNotNone(actual.service_id) + self.test.assertIsInstance(actual.service_id, ObjectId) + else: + self.test.assertIsNone(actual.service_id) + continue + if field == "hasServerConnectionId": + if spec["hasServerConnectionId"]: + self.test.assertIsNotNone(actual.server_connection_id) + self.test.assertIsInstance(actual.server_connection_id, int) + else: + self.test.assertIsNone(actual.server_connection_id) + continue + if field in ("previousDescription", "newDescription"): + if isinstance(actual, ServerDescriptionChangedEvent): + self.match_server_description( + getattr(actual, camel_to_snake(field)), spec[field] + ) + continue + if isinstance(actual, TopologyDescriptionChangedEvent): + self.match_topology_description( + getattr(actual, camel_to_snake(field)), spec[field] + ) + continue + + if field == "interruptInUseConnections": + field = "interrupt_connections" + else: + field = camel_to_snake(field) + self.test.assertEqual(getattr(actual, field), expected) + + def match_event(self, expectation, actual): + name, spec = next(iter(expectation.items())) + if name == "commandStartedEvent": + self.test.assertIsInstance(actual, CommandStartedEvent) + elif name == "commandSucceededEvent": + self.test.assertIsInstance(actual, CommandSucceededEvent) + elif name == "commandFailedEvent": + self.test.assertIsInstance(actual, CommandFailedEvent) + elif name == "poolCreatedEvent": + self.test.assertIsInstance(actual, PoolCreatedEvent) + elif name == "poolReadyEvent": + self.test.assertIsInstance(actual, PoolReadyEvent) + elif name == "poolClearedEvent": + self.test.assertIsInstance(actual, PoolClearedEvent) + self.test.assertIsInstance(actual.interrupt_connections, bool) + elif name == "poolClosedEvent": + self.test.assertIsInstance(actual, PoolClosedEvent) + elif name == "connectionCreatedEvent": + self.test.assertIsInstance(actual, ConnectionCreatedEvent) + elif name == "connectionReadyEvent": + self.test.assertIsInstance(actual, ConnectionReadyEvent) + elif name == "connectionClosedEvent": + self.test.assertIsInstance(actual, ConnectionClosedEvent) + elif name == "connectionCheckOutStartedEvent": + self.test.assertIsInstance(actual, ConnectionCheckOutStartedEvent) + elif name == "connectionCheckOutFailedEvent": + self.test.assertIsInstance(actual, ConnectionCheckOutFailedEvent) + elif name == "connectionCheckedOutEvent": + self.test.assertIsInstance(actual, ConnectionCheckedOutEvent) + elif name == "connectionCheckedInEvent": + self.test.assertIsInstance(actual, ConnectionCheckedInEvent) + elif name == "serverDescriptionChangedEvent": + self.test.assertIsInstance(actual, ServerDescriptionChangedEvent) + elif name == "serverHeartbeatStartedEvent": + self.test.assertIsInstance(actual, ServerHeartbeatStartedEvent) + elif name == "serverHeartbeatSucceededEvent": + self.test.assertIsInstance(actual, ServerHeartbeatSucceededEvent) + elif name == "serverHeartbeatFailedEvent": + self.test.assertIsInstance(actual, ServerHeartbeatFailedEvent) + elif name == "topologyDescriptionChangedEvent": + self.test.assertIsInstance(actual, TopologyDescriptionChangedEvent) + elif name == "topologyOpeningEvent": + self.test.assertIsInstance(actual, TopologyOpenedEvent) + elif name == "topologyClosedEvent": + self.test.assertIsInstance(actual, TopologyClosedEvent) + else: + raise Exception(f"Unsupported event type {name}") + + self.match_event_fields(actual, spec) + + +def coerce_result(opname, result): + """Convert a pymongo result into the spec's result format.""" + if hasattr(result, "acknowledged") and not result.acknowledged: + return {"acknowledged": False} + if opname == "bulkWrite": + return parse_bulk_write_result(result) + if opname == "clientBulkWrite": + return parse_client_bulk_write_result(result) + if opname == "insertOne": + return {"insertedId": result.inserted_id} + if opname == "insertMany": + return dict(enumerate(result.inserted_ids)) + if opname in ("deleteOne", "deleteMany"): + return {"deletedCount": result.deleted_count} + if opname in ("updateOne", "updateMany", "replaceOne"): + value = { + "matchedCount": result.matched_count, + "modifiedCount": result.modified_count, + "upsertedCount": 0 if result.upserted_id is None else 1, + } + if result.upserted_id is not None: + value["upsertedId"] = result.upserted_id + return value + return result diff --git a/test/utils.py b/test/utils.py index 174b1708ba..24673b698e 100644 --- a/test/utils.py +++ b/test/utils.py @@ -424,153 +424,6 @@ def call_count(self): return len(self._call_list) -class SpecTestCreator: - """Class to create test cases from specifications.""" - - def __init__(self, create_test, test_class, test_path): - """Create a TestCreator object. - - :Parameters: - - `create_test`: callback that returns a test case. The callback - must accept the following arguments - a dictionary containing the - entire test specification (the `scenario_def`), a dictionary - containing the specification for which the test case will be - generated (the `test_def`). - - `test_class`: the unittest.TestCase class in which to create the - test case. - - `test_path`: path to the directory containing the JSON files with - the test specifications. - """ - self._create_test = create_test - self._test_class = test_class - self.test_path = test_path - - def _ensure_min_max_server_version(self, scenario_def, method): - """Test modifier that enforces a version range for the server on a - test case. - """ - if "minServerVersion" in scenario_def: - min_ver = tuple(int(elt) for elt in scenario_def["minServerVersion"].split(".")) - if min_ver is not None: - method = client_context.require_version_min(*min_ver)(method) - - if "maxServerVersion" in scenario_def: - max_ver = tuple(int(elt) for elt in scenario_def["maxServerVersion"].split(".")) - if max_ver is not None: - method = client_context.require_version_max(*max_ver)(method) - - if "serverless" in scenario_def: - serverless = scenario_def["serverless"] - if serverless == "require": - serverless_satisfied = client_context.serverless - elif serverless == "forbid": - serverless_satisfied = not client_context.serverless - else: # unset or "allow" - serverless_satisfied = True - method = unittest.skipUnless( - serverless_satisfied, "Serverless requirement not satisfied" - )(method) - - return method - - @staticmethod - def valid_topology(run_on_req): - return client_context.is_topology_type( - run_on_req.get("topology", ["single", "replicaset", "sharded", "load-balanced"]) - ) - - @staticmethod - def min_server_version(run_on_req): - version = run_on_req.get("minServerVersion") - if version: - min_ver = tuple(int(elt) for elt in version.split(".")) - return client_context.version >= min_ver - return True - - @staticmethod - def max_server_version(run_on_req): - version = run_on_req.get("maxServerVersion") - if version: - max_ver = tuple(int(elt) for elt in version.split(".")) - return client_context.version <= max_ver - return True - - @staticmethod - def valid_auth_enabled(run_on_req): - if "authEnabled" in run_on_req: - if run_on_req["authEnabled"]: - return client_context.auth_enabled - return not client_context.auth_enabled - return True - - @staticmethod - def serverless_ok(run_on_req): - serverless = run_on_req["serverless"] - if serverless == "require": - return client_context.serverless - elif serverless == "forbid": - return not client_context.serverless - else: # unset or "allow" - return True - - def should_run_on(self, scenario_def): - run_on = scenario_def.get("runOn", []) - if not run_on: - # Always run these tests. - return True - - for req in run_on: - if ( - self.valid_topology(req) - and self.min_server_version(req) - and self.max_server_version(req) - and self.valid_auth_enabled(req) - and self.serverless_ok(req) - ): - return True - return False - - def ensure_run_on(self, scenario_def, method): - """Test modifier that enforces a 'runOn' on a test case.""" - return client_context._require( - lambda: self.should_run_on(scenario_def), "runOn not satisfied", method - ) - - def tests(self, scenario_def): - """Allow CMAP spec test to override the location of test.""" - return scenario_def["tests"] - - def create_tests(self): - for dirpath, _, filenames in os.walk(self.test_path): - dirname = os.path.split(dirpath)[-1] - - for filename in filenames: - with open(os.path.join(dirpath, filename)) as scenario_stream: - # Use tz_aware=False to match how CodecOptions decodes - # dates. - opts = json_util.JSONOptions(tz_aware=False) - scenario_def = ScenarioDict( - json_util.loads(scenario_stream.read(), json_options=opts) - ) - - test_type = os.path.splitext(filename)[0] - - # Construct test from scenario. - for test_def in self.tests(scenario_def): - test_name = "test_{}_{}_{}".format( - dirname, - test_type.replace("-", "_").replace(".", "_"), - str(test_def["description"].replace(" ", "_").replace(".", "_")), - ) - - new_test = self._create_test(scenario_def, test_def, test_name) - new_test = self._ensure_min_max_server_version(scenario_def, new_test) - new_test = self.ensure_run_on(scenario_def, new_test) - - new_test.__name__ = test_name - setattr(self._test_class, new_test.__name__, new_test) - - def ensure_all_connected(client: MongoClient) -> None: """Ensure that the client's connection pool has socket connections to all members of a replica set. Raises ConfigurationError when called with a diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index 06a40351cd..8a061de0b1 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -15,8 +15,12 @@ """Utilities for testing driver specs.""" from __future__ import annotations +import asyncio import functools +import os import threading +import unittest +from asyncio import iscoroutinefunction from collections import abc from test import IntegrationTest, client_context, client_knobs from test.utils import ( @@ -24,6 +28,7 @@ CompareType, EventListener, OvertCommandListener, + ScenarioDict, ServerAndTopologyEventListener, camel_to_snake, camel_to_snake_args, @@ -32,11 +37,12 @@ ) from typing import List -from bson import ObjectId, decode, encode +from bson import ObjectId, decode, encode, json_util from bson.binary import Binary from bson.int64 import Int64 from bson.son import SON from gridfs import GridFSBucket +from gridfs.synchronous.grid_file import GridFSBucket from pymongo.errors import BulkWriteError, OperationFailure, PyMongoError from pymongo.read_concern import ReadConcern from pymongo.read_preferences import ReadPreference @@ -83,6 +89,161 @@ def run(self): self.stop() +class SpecTestCreator: + """Class to create test cases from specifications.""" + + def __init__(self, create_test, test_class, test_path): + """Create a TestCreator object. + + :Parameters: + - `create_test`: callback that returns a test case. The callback + must accept the following arguments - a dictionary containing the + entire test specification (the `scenario_def`), a dictionary + containing the specification for which the test case will be + generated (the `test_def`). + - `test_class`: the unittest.TestCase class in which to create the + test case. + - `test_path`: path to the directory containing the JSON files with + the test specifications. + """ + self._create_test = create_test + self._test_class = test_class + self.test_path = test_path + + def _ensure_min_max_server_version(self, scenario_def, method): + """Test modifier that enforces a version range for the server on a + test case. + """ + if "minServerVersion" in scenario_def: + min_ver = tuple(int(elt) for elt in scenario_def["minServerVersion"].split(".")) + if min_ver is not None: + method = client_context.require_version_min(*min_ver)(method) + + if "maxServerVersion" in scenario_def: + max_ver = tuple(int(elt) for elt in scenario_def["maxServerVersion"].split(".")) + if max_ver is not None: + method = client_context.require_version_max(*max_ver)(method) + + if "serverless" in scenario_def: + serverless = scenario_def["serverless"] + if serverless == "require": + serverless_satisfied = client_context.serverless + elif serverless == "forbid": + serverless_satisfied = not client_context.serverless + else: # unset or "allow" + serverless_satisfied = True + method = unittest.skipUnless( + serverless_satisfied, "Serverless requirement not satisfied" + )(method) + + return method + + @staticmethod + def valid_topology(run_on_req): + return client_context.is_topology_type( + run_on_req.get("topology", ["single", "replicaset", "sharded", "load-balanced"]) + ) + + @staticmethod + def min_server_version(run_on_req): + version = run_on_req.get("minServerVersion") + if version: + min_ver = tuple(int(elt) for elt in version.split(".")) + return client_context.version >= min_ver + return True + + @staticmethod + def max_server_version(run_on_req): + version = run_on_req.get("maxServerVersion") + if version: + max_ver = tuple(int(elt) for elt in version.split(".")) + return client_context.version <= max_ver + return True + + @staticmethod + def valid_auth_enabled(run_on_req): + if "authEnabled" in run_on_req: + if run_on_req["authEnabled"]: + return client_context.auth_enabled + return not client_context.auth_enabled + return True + + @staticmethod + def serverless_ok(run_on_req): + serverless = run_on_req["serverless"] + if serverless == "require": + return client_context.serverless + elif serverless == "forbid": + return not client_context.serverless + else: # unset or "allow" + return True + + def should_run_on(self, scenario_def): + run_on = scenario_def.get("runOn", []) + if not run_on: + # Always run these tests. + return True + + for req in run_on: + if ( + self.valid_topology(req) + and self.min_server_version(req) + and self.max_server_version(req) + and self.valid_auth_enabled(req) + and self.serverless_ok(req) + ): + return True + return False + + def ensure_run_on(self, scenario_def, method): + """Test modifier that enforces a 'runOn' on a test case.""" + + def predicate(): + return self.should_run_on(scenario_def) + + return client_context._require(predicate, "runOn not satisfied", method) + + def tests(self, scenario_def): + """Allow CMAP spec test to override the location of test.""" + return scenario_def["tests"] + + def _create_tests(self): + for dirpath, _, filenames in os.walk(self.test_path): + dirname = os.path.split(dirpath)[-1] + + for filename in filenames: + with open(os.path.join(dirpath, filename)) as scenario_stream: # noqa: ASYNC101, RUF100 + # Use tz_aware=False to match how CodecOptions decodes + # dates. + opts = json_util.JSONOptions(tz_aware=False) + scenario_def = ScenarioDict( + json_util.loads(scenario_stream.read(), json_options=opts) + ) + + test_type = os.path.splitext(filename)[0] + + # Construct test from scenario. + for test_def in self.tests(scenario_def): + test_name = "test_{}_{}_{}".format( + dirname, + test_type.replace("-", "_").replace(".", "_"), + str(test_def["description"].replace(" ", "_").replace(".", "_")), + ) + + new_test = self._create_test(scenario_def, test_def, test_name) + new_test = self._ensure_min_max_server_version(scenario_def, new_test) + new_test = self.ensure_run_on(scenario_def, new_test) + + new_test.__name__ = test_name + setattr(self._test_class, new_test.__name__, new_test) + + def create_tests(self): + if _IS_SYNC: + self._create_tests() + else: + asyncio.run(self._create_tests()) + + class SpecRunner(IntegrationTest): mongos_clients: List knobs: client_knobs @@ -312,7 +473,10 @@ def run_operation(self, sessions, collection, operation): args.update(arguments) arguments = args - result = cmd(**dict(arguments)) + if not _IS_SYNC and iscoroutinefunction(cmd): + result = cmd(**dict(arguments)) + else: + result = cmd(**dict(arguments)) # Cleanup open change stream cursors. if name == "watch": self.addCleanup(result.close) @@ -583,7 +747,7 @@ def run_scenario(self, scenario_def, test): read_preference=ReadPreference.PRIMARY, read_concern=ReadConcern("local"), ) - actual_data = (outcome_coll.find(sort=[("_id", 1)])).to_list() + actual_data = outcome_coll.find(sort=[("_id", 1)]).to_list() # The expected data needs to be the left hand side here otherwise # CompareType(Binary) doesn't work. diff --git a/tools/synchro.py b/tools/synchro.py index c3c0b568ed..f460b348c4 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -105,6 +105,8 @@ "PyMongo|c|async": "PyMongo|c", "AsyncTestGridFile": "TestGridFile", "AsyncTestGridFileNoConnect": "TestGridFileNoConnect", + "AsyncTestSpec": "TestSpec", + "AsyncSpecTestCreator": "SpecTestCreator", "async_set_fail_point": "set_fail_point", "async_ensure_all_connected": "ensure_all_connected", "async_repl_set_step_down": "repl_set_step_down", @@ -194,8 +196,14 @@ def async_only_test(f: str) -> bool: "test_client_context.py", "test_collation.py", "test_collection.py", + "test_command_logging.py", + "test_command_logging.py", + "test_command_monitoring.py", + "test_comment.py", "test_common.py", + "test_connection_logging.py", "test_connections_survive_primary_stepdown_spec.py", + "test_crud_unified.py", "test_cursor.py", "test_database.py", "test_encryption.py", @@ -207,6 +215,7 @@ def async_only_test(f: str) -> bool: "test_retryable_writes.py", "test_session.py", "test_transactions.py", + "unified_format.py", ] sync_test_files = [ From 7aac9d5fa2910d50e86960cbf0b43d6bd65b7393 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 15 Oct 2024 15:31:04 -0400 Subject: [PATCH 03/14] PYTHON-4871 - Fix async waitForEvent (#1933) --- test/asynchronous/unified_format.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 42bda59cb2..2ff38f06e9 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -50,6 +50,7 @@ ) from test.utils import ( async_get_pool, + async_wait_until, camel_to_snake, camel_to_snake_args, parse_spec_options, @@ -1144,13 +1145,13 @@ def _testOperation_assertEventCount(self, spec): client, event, count = spec["client"], spec["event"], spec["count"] self.assertEqual(self._event_count(client, event), count, f"expected {count} not {event!r}") - def _testOperation_waitForEvent(self, spec): + async def _testOperation_waitForEvent(self, spec): """Run the waitForEvent test operation. Wait for a number of events to be published, or fail. """ client, event, count = spec["client"], spec["event"], spec["count"] - wait_until( + await async_wait_until( lambda: self._event_count(client, event) >= count, f"find {count} {event} event(s)", ) From e4ebfa4c5673d3357e827c47ebc439ffdc148481 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 16 Oct 2024 10:57:23 -0400 Subject: [PATCH 04/14] PYTHON-4843 - All tests should isolate state within a single async IO loop (#1928) Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com> Co-authored-by: Steven Silvester Co-authored-by: Shane Harvey --- test/__init__.py | 82 ++---------- test/asynchronous/__init__.py | 86 +++---------- test/asynchronous/test_bulk.py | 36 ++---- test/asynchronous/test_change_stream.py | 43 ++----- test/asynchronous/test_client.py | 9 +- test/asynchronous/test_collation.py | 30 ++--- test/asynchronous/test_collection.py | 33 ++--- ...nnections_survive_primary_stepdown_spec.py | 28 ++-- test/asynchronous/test_cursor.py | 4 - test/asynchronous/test_database.py | 3 +- test/asynchronous/test_encryption.py | 121 +++++++----------- test/asynchronous/test_grid_file.py | 1 + test/asynchronous/test_monitoring.py | 47 ++++--- test/asynchronous/test_retryable_writes.py | 65 ++++------ test/asynchronous/test_session.py | 32 ++--- test/asynchronous/test_transactions.py | 17 +-- test/asynchronous/unified_format.py | 58 ++++----- test/asynchronous/utils_spec_runner.py | 26 ++-- test/test_bulk.py | 32 ++--- test/test_change_stream.py | 39 ++---- test/test_client.py | 9 +- test/test_collation.py | 28 ++-- test/test_collection.py | 33 ++--- ...nnections_survive_primary_stepdown_spec.py | 28 ++-- test/test_cursor.py | 4 - test/test_custom_types.py | 23 ++-- test/test_database.py | 1 + test/test_encryption.py | 119 +++++++---------- test/test_examples.py | 13 +- test/test_grid_file.py | 1 + test/test_gridfs.py | 20 ++- test/test_gridfs_bucket.py | 14 +- test/test_monitoring.py | 45 +++---- test/test_read_concern.py | 20 +-- test/test_retryable_writes.py | 65 ++++------ test/test_sdam_monitoring_spec.py | 2 +- test/test_session.py | 32 ++--- test/test_threads.py | 1 + test/test_transactions.py | 15 +-- test/test_typing.py | 7 +- test/unified_format.py | 56 ++++---- test/utils_spec_runner.py | 26 ++-- 42 files changed, 473 insertions(+), 881 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index c55eb74c9d..940518c2c5 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1115,26 +1115,10 @@ def enable_replication(self, client): class UnitTest(PyMongoTestCase): """Async base class for TestCases that don't require a connection to MongoDB.""" - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod - def _setup_class(cls): + def setUp(self) -> None: pass - @classmethod - def _tearDown_class(cls): + def tearDown(self) -> None: pass @@ -1145,37 +1129,20 @@ class IntegrationTest(PyMongoTestCase): db: Database credentials: Dict[str, str] - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod @client_context.require_connection - def _setup_class(cls): - if client_context.load_balancer and not getattr(cls, "RUN_ON_LOAD_BALANCER", False): + def setUp(self) -> None: + if not _IS_SYNC: + reset_client_context() + if client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") - if client_context.serverless and not getattr(cls, "RUN_ON_SERVERLESS", False): + if client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): raise SkipTest("this test does not support serverless") - cls.client = client_context.client - cls.db = cls.client.pymongo_test + self.client = client_context.client + self.db = self.client.pymongo_test if client_context.auth_enabled: - cls.credentials = {"username": db_user, "password": db_pwd} + self.credentials = {"username": db_user, "password": db_pwd} else: - cls.credentials = {} - - @classmethod - def _tearDown_class(cls): - pass + self.credentials = {} def cleanup_colls(self, *collections): """Cleanup collections faster than drop_collection.""" @@ -1201,37 +1168,14 @@ class MockClientTest(UnitTest): # MockClients tests that use replicaSet, directConnection=True, pass # multiple seed addresses, or wait for heartbeat events are incompatible # with loadBalanced=True. - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod @client_context.require_no_load_balancer - def _setup_class(cls): - pass - - @classmethod - def _tearDown_class(cls): - pass - - def setUp(self): + def setUp(self) -> None: super().setUp() self.client_knobs = client_knobs(heartbeat_frequency=0.001, min_heartbeat_interval=0.001) - self.client_knobs.enable() - def tearDown(self): + def tearDown(self) -> None: self.client_knobs.disable() super().tearDown() diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 58e69c7c58..8d1e3e1911 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -1133,26 +1133,10 @@ async def enable_replication(self, client): class AsyncUnitTest(AsyncPyMongoTestCase): """Async base class for TestCases that don't require a connection to MongoDB.""" - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod - async def _setup_class(cls): + async def asyncSetUp(self) -> None: pass - @classmethod - async def _tearDown_class(cls): + async def asyncTearDown(self) -> None: pass @@ -1163,37 +1147,20 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase): db: AsyncDatabase credentials: Dict[str, str] - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod @async_client_context.require_connection - async def _setup_class(cls): - if async_client_context.load_balancer and not getattr(cls, "RUN_ON_LOAD_BALANCER", False): + async def asyncSetUp(self) -> None: + if not _IS_SYNC: + await reset_client_context() + if async_client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") - if async_client_context.serverless and not getattr(cls, "RUN_ON_SERVERLESS", False): + if async_client_context.serverless and not getattr(self, "RUN_ON_SERVERLESS", False): raise SkipTest("this test does not support serverless") - cls.client = async_client_context.client - cls.db = cls.client.pymongo_test + self.client = async_client_context.client + self.db = self.client.pymongo_test if async_client_context.auth_enabled: - cls.credentials = {"username": db_user, "password": db_pwd} + self.credentials = {"username": db_user, "password": db_pwd} else: - cls.credentials = {} - - @classmethod - async def _tearDown_class(cls): - pass + self.credentials = {} async def cleanup_colls(self, *collections): """Cleanup collections faster than drop_collection.""" @@ -1219,39 +1186,16 @@ class AsyncMockClientTest(AsyncUnitTest): # MockClients tests that use replicaSet, directConnection=True, pass # multiple seed addresses, or wait for heartbeat events are incompatible # with loadBalanced=True. - @classmethod - def setUpClass(cls): - if _IS_SYNC: - cls._setup_class() - else: - asyncio.run(cls._setup_class()) - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls._tearDown_class() - else: - asyncio.run(cls._tearDown_class()) - - @classmethod @async_client_context.require_no_load_balancer - async def _setup_class(cls): - pass - - @classmethod - async def _tearDown_class(cls): - pass - - def setUp(self): - super().setUp() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() self.client_knobs = client_knobs(heartbeat_frequency=0.001, min_heartbeat_interval=0.001) - self.client_knobs.enable() - def tearDown(self): + async def asyncTearDown(self) -> None: self.client_knobs.disable() - super().tearDown() + await super().asyncTearDown() async def async_setup(): diff --git a/test/asynchronous/test_bulk.py b/test/asynchronous/test_bulk.py index 42a3311072..e01dd53d7e 100644 --- a/test/asynchronous/test_bulk.py +++ b/test/asynchronous/test_bulk.py @@ -42,15 +42,11 @@ class AsyncBulkTestBase(AsyncIntegrationTest): coll: AsyncCollection coll_w0: AsyncCollection - @classmethod - async def _setup_class(cls): - await super()._setup_class() - cls.coll = cls.db.test - cls.coll_w0 = cls.coll.with_options(write_concern=WriteConcern(w=0)) - async def asyncSetUp(self): - super().setUp() + await super().asyncSetUp() + self.coll = self.db.test await self.coll.drop() + self.coll_w0 = self.coll.with_options(write_concern=WriteConcern(w=0)) def assertEqualResponse(self, expected, actual): """Compare response from bulk.execute() to expected response.""" @@ -787,14 +783,10 @@ async def test_large_inserts_unordered(self): class AsyncBulkAuthorizationTestBase(AsyncBulkTestBase): - @classmethod @async_client_context.require_auth @async_client_context.require_no_api_version - async def _setup_class(cls): - await super()._setup_class() - async def asyncSetUp(self): - super().setUp() + await super().asyncSetUp() await async_client_context.create_user(self.db.name, "readonly", "pw", ["read"]) await self.db.command( "createRole", @@ -937,21 +929,19 @@ class AsyncTestBulkWriteConcern(AsyncBulkTestBase): w: Optional[int] secondary: AsyncMongoClient - @classmethod - async def _setup_class(cls): - await super()._setup_class() - cls.w = async_client_context.w - cls.secondary = None - if cls.w is not None and cls.w > 1: + async def asyncSetUp(self): + await super().asyncSetUp() + self.w = async_client_context.w + self.secondary = None + if self.w is not None and self.w > 1: for member in (await async_client_context.hello)["hosts"]: if member != (await async_client_context.hello)["primary"]: - cls.secondary = await cls.unmanaged_async_single_client(*partition_node(member)) + self.secondary = await self.async_single_client(*partition_node(member)) break - @classmethod - async def async_tearDownClass(cls): - if cls.secondary: - await cls.secondary.close() + async def asyncTearDown(self): + if self.secondary: + await self.secondary.close() async def cause_wtimeout(self, requests, ordered): if not async_client_context.test_commands_enabled: diff --git a/test/asynchronous/test_change_stream.py b/test/asynchronous/test_change_stream.py index 883ed72c4c..db8a74f55a 100644 --- a/test/asynchronous/test_change_stream.py +++ b/test/asynchronous/test_change_stream.py @@ -835,18 +835,16 @@ async def test_split_large_change(self): class TestClusterAsyncChangeStream(TestAsyncChangeStreamBase, APITestsMixin): dbs: list - @classmethod @async_client_context.require_version_min(4, 0, 0, -1) @async_client_context.require_change_streams - async def _setup_class(cls): - await super()._setup_class() - cls.dbs = [cls.db, cls.client.pymongo_test_2] + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.dbs = [self.db, self.client.pymongo_test_2] - @classmethod - async def _tearDown_class(cls): - for db in cls.dbs: - await cls.client.drop_database(db) - await super()._tearDown_class() + async def asyncTearDown(self): + for db in self.dbs: + await self.client.drop_database(db) + await super().asyncTearDown() async def change_stream_with_client(self, client, *args, **kwargs): return await client.watch(*args, **kwargs) @@ -897,11 +895,10 @@ async def test_full_pipeline(self): class TestAsyncDatabaseAsyncChangeStream(TestAsyncChangeStreamBase, APITestsMixin): - @classmethod @async_client_context.require_version_min(4, 0, 0, -1) @async_client_context.require_change_streams - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() async def change_stream_with_client(self, client, *args, **kwargs): return await client[self.db.name].watch(*args, **kwargs) @@ -987,12 +984,9 @@ async def test_isolation(self): class TestAsyncCollectionAsyncChangeStream( TestAsyncChangeStreamBase, APITestsMixin, ProseSpecTestsMixin ): - @classmethod @async_client_context.require_change_streams - async def _setup_class(cls): - await super()._setup_class() - async def asyncSetUp(self): + await super().asyncSetUp() # Use a new collection for each test. await self.watched_collection().drop() await self.watched_collection().insert_one({}) @@ -1132,20 +1126,11 @@ class TestAllLegacyScenarios(AsyncIntegrationTest): RUN_ON_LOAD_BALANCER = True listener: AllowListEventListener - @classmethod @async_client_context.require_connection - async def _setup_class(cls): - await super()._setup_class() - cls.listener = AllowListEventListener("aggregate", "getMore") - cls.client = await cls.unmanaged_async_rs_or_single_client(event_listeners=[cls.listener]) - - @classmethod - async def _tearDown_class(cls): - await cls.client.close() - await super()._tearDown_class() - - def asyncSetUp(self): - super().asyncSetUp() + async def asyncSetUp(self): + await super().asyncSetUp() + self.listener = AllowListEventListener("aggregate", "getMore") + self.client = await self.async_rs_or_single_client(event_listeners=[self.listener]) self.listener.reset() async def asyncSetUpCluster(self, scenario_dict): diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index ce396997e3..47cbff6d5b 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -130,16 +130,11 @@ class AsyncClientUnitTest(AsyncUnitTest): client: AsyncMongoClient - @classmethod - async def _setup_class(cls): - cls.client = await cls.unmanaged_async_rs_or_single_client( + async def asyncSetUp(self) -> None: + self.client = await self.async_rs_or_single_client( connect=False, serverSelectionTimeoutMS=100 ) - @classmethod - async def _tearDown_class(cls): - await cls.client.close() - @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self._caplog = caplog diff --git a/test/asynchronous/test_collation.py b/test/asynchronous/test_collation.py index be3ea22e42..abbca1aff9 100644 --- a/test/asynchronous/test_collation.py +++ b/test/asynchronous/test_collation.py @@ -97,28 +97,22 @@ class TestCollation(AsyncIntegrationTest): warn_context: Any collation: Collation - @classmethod @async_client_context.require_connection - async def _setup_class(cls): - await super()._setup_class() - cls.listener = EventListener() - cls.client = await cls.unmanaged_async_rs_or_single_client(event_listeners=[cls.listener]) - cls.db = cls.client.pymongo_test - cls.collation = Collation("en_US") - cls.warn_context = warnings.catch_warnings() - cls.warn_context.__enter__() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.listener = EventListener() + self.client = await self.async_rs_or_single_client(event_listeners=[self.listener]) + self.db = self.client.pymongo_test + self.collation = Collation("en_US") + self.warn_context = warnings.catch_warnings() + self.warn_context.__enter__() warnings.simplefilter("ignore", DeprecationWarning) - @classmethod - async def _tearDown_class(cls): - cls.warn_context.__exit__() - cls.warn_context = None - await cls.client.close() - await super()._tearDown_class() - - def tearDown(self): + async def asyncTearDown(self) -> None: + self.warn_context.__exit__() + self.warn_context = None self.listener.reset() - super().tearDown() + await super().asyncTearDown() def last_command_started(self): return self.listener.started_events[-1].command diff --git a/test/asynchronous/test_collection.py b/test/asynchronous/test_collection.py index 470425f4ce..a2ed4de388 100644 --- a/test/asynchronous/test_collection.py +++ b/test/asynchronous/test_collection.py @@ -86,14 +86,10 @@ class TestCollectionNoConnect(AsyncUnitTest): db: AsyncDatabase client: AsyncMongoClient - @classmethod - async def _setup_class(cls): - cls.client = AsyncMongoClient(connect=False) - cls.db = cls.client.pymongo_test - - @classmethod - async def _tearDown_class(cls): - await cls.client.close() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.client = self.simple_client(connect=False) + self.db = self.client.pymongo_test def test_collection(self): self.assertRaises(TypeError, AsyncCollection, self.db, 5) @@ -163,27 +159,14 @@ def test_iteration(self): class AsyncTestCollection(AsyncIntegrationTest): w: int - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.w = async_client_context.w # type: ignore - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls.db.drop_collection("test_large_limit") # type: ignore[unused-coroutine] - else: - asyncio.run(cls.async_tearDownClass()) - - @classmethod - async def async_tearDownClass(cls): - await cls.db.drop_collection("test_large_limit") - async def asyncSetUp(self): - await self.db.test.drop() + await super().asyncSetUp() + self.w = async_client_context.w # type: ignore async def asyncTearDown(self): await self.db.test.drop() + await self.db.drop_collection("test_large_limit") + await super().asyncTearDown() @contextlib.contextmanager def write_concern_collection(self): diff --git a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py index dc04cb28a7..ffff428379 100644 --- a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py +++ b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py @@ -44,30 +44,22 @@ class TestAsyncConnectionsSurvivePrimaryStepDown(AsyncIntegrationTest): listener: CMAPListener coll: AsyncCollection - @classmethod + async def asyncTearDown(self): + await reset_client_context() + @async_client_context.require_replica_set - async def _setup_class(cls): - await super()._setup_class() - cls.listener = CMAPListener() - cls.client = await cls.unmanaged_async_rs_or_single_client( - event_listeners=[cls.listener], retryWrites=False, heartbeatFrequencyMS=500 + async def asyncSetUp(self): + self.listener = CMAPListener() + self.client = await self.async_rs_or_single_client( + event_listeners=[self.listener], retryWrites=False, heartbeatFrequencyMS=500 ) # Ensure connections to all servers in replica set. This is to test # that the is_writable flag is properly updated for connections that # survive a replica set election. - await async_ensure_all_connected(cls.client) - cls.listener.reset() - - cls.db = cls.client.get_database("step-down", write_concern=WriteConcern("majority")) - cls.coll = cls.db.get_collection("step-down", write_concern=WriteConcern("majority")) - - @classmethod - async def _tearDown_class(cls): - await cls.client.close() - await reset_client_context() - - async def asyncSetUp(self): + await async_ensure_all_connected(self.client) + self.db = self.client.get_database("step-down", write_concern=WriteConcern("majority")) + self.coll = self.db.get_collection("step-down", write_concern=WriteConcern("majority")) # Note that all ops use same write-concern as self.db (majority). await self.db.drop_collection("step-down") await self.db.create_collection("step-down") diff --git a/test/asynchronous/test_cursor.py b/test/asynchronous/test_cursor.py index b1ca8855de..09955ca66f 100644 --- a/test/asynchronous/test_cursor.py +++ b/test/asynchronous/test_cursor.py @@ -1647,10 +1647,6 @@ async def test_monitoring(self): class TestRawBatchCommandCursor(AsyncIntegrationTest): - @classmethod - async def _setup_class(cls): - await super()._setup_class() - async def test_aggregate_raw(self): c = self.db.test await c.drop() diff --git a/test/asynchronous/test_database.py b/test/asynchronous/test_database.py index 61369c8542..b5a5960420 100644 --- a/test/asynchronous/test_database.py +++ b/test/asynchronous/test_database.py @@ -717,7 +717,8 @@ def test_with_options(self): class TestDatabaseAggregation(AsyncIntegrationTest): - def setUp(self): + async def asyncSetUp(self): + await super().asyncSetUp() self.pipeline: List[Mapping[str, Any]] = [ {"$listLocalSessions": {}}, {"$limit": 1}, diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index 88b005c4b3..d75bad6862 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -211,11 +211,10 @@ async def test_kwargs(self): class AsyncEncryptionIntegrationTest(AsyncIntegrationTest): """Base class for encryption integration tests.""" - @classmethod @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") @async_client_context.require_version_min(4, 2, -1) - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() def assertEncrypted(self, val): self.assertIsInstance(val, Binary) @@ -430,10 +429,9 @@ async def test_upsert_uuid_standard_encrypt(self): class TestClientMaxWireVersion(AsyncIntegrationTest): - @classmethod @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self): + await super().asyncSetUp() @async_client_context.require_version_max(4, 0, 99) async def test_raise_max_wire_version_error(self): @@ -818,17 +816,16 @@ class TestDataKeyDoubleEncryption(AsyncEncryptionIntegrationTest): "local": None, } - @classmethod @unittest.skipUnless( any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]), "No environment credentials are set", ) - async def _setup_class(cls): - await super()._setup_class() - cls.listener = OvertCommandListener() - cls.client = await cls.unmanaged_async_rs_or_single_client(event_listeners=[cls.listener]) - await cls.client.db.coll.drop() - cls.vault = await create_key_vault(cls.client.keyvault.datakeys) + async def asyncSetUp(self): + await super().asyncSetUp() + self.listener = OvertCommandListener() + self.client = await self.async_rs_or_single_client(event_listeners=[self.listener]) + await self.client.db.coll.drop() + self.vault = await create_key_vault(self.client.keyvault.datakeys) # Configure the encrypted field via the local schema_map option. schemas = { @@ -846,25 +843,22 @@ async def _setup_class(cls): } } opts = AutoEncryptionOpts( - cls.KMS_PROVIDERS, "keyvault.datakeys", schema_map=schemas, kms_tls_options=KMS_TLS_OPTS + self.KMS_PROVIDERS, + "keyvault.datakeys", + schema_map=schemas, + kms_tls_options=KMS_TLS_OPTS, ) - cls.client_encrypted = await cls.unmanaged_async_rs_or_single_client( + self.client_encrypted = await self.async_rs_or_single_client( auto_encryption_opts=opts, uuidRepresentation="standard" ) - cls.client_encryption = cls.unmanaged_create_client_encryption( - cls.KMS_PROVIDERS, "keyvault.datakeys", cls.client, OPTS, kms_tls_options=KMS_TLS_OPTS + self.client_encryption = self.create_client_encryption( + self.KMS_PROVIDERS, "keyvault.datakeys", self.client, OPTS, kms_tls_options=KMS_TLS_OPTS ) - - @classmethod - async def _tearDown_class(cls): - await cls.vault.drop() - await cls.client.close() - await cls.client_encrypted.close() - await cls.client_encryption.close() - - def setUp(self): self.listener.reset() + async def asyncTearDown(self) -> None: + await self.vault.drop() + async def run_test(self, provider_name): # Create data key. master_key: Any = self.MASTER_KEYS[provider_name] @@ -1011,10 +1005,9 @@ async def test_views_are_prohibited(self): class TestCorpus(AsyncEncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set") - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self): + await super().asyncSetUp() @staticmethod def kms_providers(): @@ -1188,12 +1181,11 @@ class TestBsonSizeBatches(AsyncEncryptionIntegrationTest): client_encrypted: AsyncMongoClient listener: OvertCommandListener - @classmethod - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self): + await super().asyncSetUp() db = async_client_context.client.db - cls.coll = db.coll - await cls.coll.drop() + self.coll = db.coll + await self.coll.drop() # Configure the encrypted 'db.coll' collection via jsonSchema. json_schema = json_data("limits", "limits-schema.json") await db.create_collection( @@ -1211,17 +1203,14 @@ async def _setup_class(cls): await coll.insert_one(json_data("limits", "limits-key.json")) opts = AutoEncryptionOpts({"local": {"key": LOCAL_MASTER_KEY}}, "keyvault.datakeys") - cls.listener = OvertCommandListener() - cls.client_encrypted = await cls.unmanaged_async_rs_or_single_client( - auto_encryption_opts=opts, event_listeners=[cls.listener] + self.listener = OvertCommandListener() + self.client_encrypted = await self.async_rs_or_single_client( + auto_encryption_opts=opts, event_listeners=[self.listener] ) - cls.coll_encrypted = cls.client_encrypted.db.coll + self.coll_encrypted = self.client_encrypted.db.coll - @classmethod - async def _tearDown_class(cls): - await cls.coll_encrypted.drop() - await cls.client_encrypted.close() - await super()._tearDown_class() + async def asyncTearDown(self) -> None: + await self.coll_encrypted.drop() async def test_01_insert_succeeds_under_2MiB(self): doc = {"_id": "over_2mib_under_16mib", "unencrypted": "a" * _2_MiB} @@ -1285,15 +1274,12 @@ async def test_06_insert_fails_over_16MiB(self): class TestCustomEndpoint(AsyncEncryptionIntegrationTest): """Prose tests for creating data keys with a custom endpoint.""" - @classmethod @unittest.skipUnless( any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]), "No environment credentials are set", ) - async def _setup_class(cls): - await super()._setup_class() - - def setUp(self): + async def asyncSetUp(self): + await super().asyncSetUp() kms_providers = { "aws": AWS_CREDS, "azure": AZURE_CREDS, @@ -1322,10 +1308,6 @@ def setUp(self): self._kmip_host_error = None self._invalid_host_error = None - async def asyncTearDown(self): - await self.client_encryption.close() - await self.client_encryption_invalid.close() - async def run_test_expected_success(self, provider_name, master_key): data_key_id = await self.client_encryption.create_data_key( provider_name, master_key=master_key @@ -1501,6 +1483,7 @@ class AzureGCPEncryptionTestMixin(AsyncEncryptionIntegrationTest): client: AsyncMongoClient async def asyncSetUp(self): + self.client = self.simple_client() keyvault = self.client.get_database(self.KEYVAULT_DB).get_collection(self.KEYVAULT_COLL) await create_key_vault(keyvault, self.DEK) @@ -1559,13 +1542,12 @@ async def _test_automatic(self, expectation_extjson, payload): class TestAzureEncryption(AzureGCPEncryptionTestMixin, AsyncEncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(AZURE_CREDS.values()), "Azure environment credentials are not set") - async def _setup_class(cls): - cls.KMS_PROVIDER_MAP = {"azure": AZURE_CREDS} - cls.DEK = json_data(BASE, "custom", "azure-dek.json") - cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") - await super()._setup_class() + async def asyncSetUp(self): + self.KMS_PROVIDER_MAP = {"azure": AZURE_CREDS} + self.DEK = json_data(BASE, "custom", "azure-dek.json") + self.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") + await super().asyncSetUp() async def test_explicit(self): return await self._test_explicit( @@ -1585,13 +1567,12 @@ async def test_automatic(self): class TestGCPEncryption(AzureGCPEncryptionTestMixin, AsyncEncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(GCP_CREDS.values()), "GCP environment credentials are not set") - async def _setup_class(cls): - cls.KMS_PROVIDER_MAP = {"gcp": GCP_CREDS} - cls.DEK = json_data(BASE, "custom", "gcp-dek.json") - cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") - await super()._setup_class() + async def asyncSetUp(self): + self.KMS_PROVIDER_MAP = {"gcp": GCP_CREDS} + self.DEK = json_data(BASE, "custom", "gcp-dek.json") + self.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") + await super().asyncSetUp() async def test_explicit(self): return await self._test_explicit( @@ -3089,17 +3070,11 @@ class TestNoSessionsSupport(AsyncEncryptionIntegrationTest): mongocryptd_client: AsyncMongoClient MONGOCRYPTD_PORT = 27020 - @classmethod @unittest.skipIf(os.environ.get("TEST_CRYPT_SHARED"), "crypt_shared lib is installed") - async def _setup_class(cls): - await super()._setup_class() - start_mongocryptd(cls.MONGOCRYPTD_PORT) - - @classmethod - async def _tearDown_class(cls): - await super()._tearDown_class() - async def asyncSetUp(self) -> None: + await super().asyncSetUp() + start_mongocryptd(self.MONGOCRYPTD_PORT) + self.listener = OvertCommandListener() self.mongocryptd_client = self.simple_client( f"mongodb://localhost:{self.MONGOCRYPTD_PORT}", event_listeners=[self.listener] diff --git a/test/asynchronous/test_grid_file.py b/test/asynchronous/test_grid_file.py index 9c57c15c5a..14446106e0 100644 --- a/test/asynchronous/test_grid_file.py +++ b/test/asynchronous/test_grid_file.py @@ -97,6 +97,7 @@ def test_grid_in_custom_opts(self): class AsyncTestGridFile(AsyncIntegrationTest): async def asyncSetUp(self): + await super().asyncSetUp() await self.cleanup_colls(self.db.fs.files, self.db.fs.chunks) async def test_basic(self): diff --git a/test/asynchronous/test_monitoring.py b/test/asynchronous/test_monitoring.py index b5d8708dc3..a5f991b2f0 100644 --- a/test/asynchronous/test_monitoring.py +++ b/test/asynchronous/test_monitoring.py @@ -51,22 +51,16 @@ class AsyncTestCommandMonitoring(AsyncIntegrationTest): listener: EventListener @classmethod - @async_client_context.require_connection - async def _setup_class(cls): - await super()._setup_class() + def setUpClass(cls) -> None: cls.listener = EventListener() - cls.client = await cls.unmanaged_async_rs_or_single_client( - event_listeners=[cls.listener], retryWrites=False - ) - @classmethod - async def _tearDown_class(cls): - await cls.client.close() - await super()._tearDown_class() - - async def asyncTearDown(self): + @async_client_context.require_connection + async def asyncSetUp(self) -> None: + await super().asyncSetUp() self.listener.reset() - await super().asyncTearDown() + self.client = await self.async_rs_or_single_client( + event_listeners=[self.listener], retryWrites=False + ) async def test_started_simple(self): await self.client.pymongo_test.command("ping") @@ -1137,27 +1131,30 @@ class AsyncTestGlobalListener(AsyncIntegrationTest): saved_listeners: Any @classmethod - @async_client_context.require_connection - async def _setup_class(cls): - await super()._setup_class() + def setUpClass(cls) -> None: cls.listener = EventListener() # We plan to call register(), which internally modifies _LISTENERS. cls.saved_listeners = copy.deepcopy(monitoring._LISTENERS) monitoring.register(cls.listener) - cls.client = await cls.unmanaged_async_single_client() - # Get one (authenticated) socket in the pool. - await cls.client.pymongo_test.command("ping") - - @classmethod - async def _tearDown_class(cls): - monitoring._LISTENERS = cls.saved_listeners - await cls.client.close() - await super()._tearDown_class() + @async_client_context.require_connection async def asyncSetUp(self): await super().asyncSetUp() + self.listener = EventListener() + # We plan to call register(), which internally modifies _LISTENERS. + self.saved_listeners = copy.deepcopy(monitoring._LISTENERS) + monitoring.register(self.listener) + self.client = await self.async_single_client() + # Get one (authenticated) socket in the pool. + await self.client.pymongo_test.command("ping") + + async def asyncTearDown(self) -> None: self.listener.reset() + @classmethod + def tearDownClass(cls): + monitoring._LISTENERS = cls.saved_listeners + async def test_simple(self): await self.client.pymongo_test.command("ping") started = self.listener.started_events[0] diff --git a/test/asynchronous/test_retryable_writes.py b/test/asynchronous/test_retryable_writes.py index accbbd003f..746f23ea48 100644 --- a/test/asynchronous/test_retryable_writes.py +++ b/test/asynchronous/test_retryable_writes.py @@ -133,34 +133,27 @@ class IgnoreDeprecationsTest(AsyncIntegrationTest): RUN_ON_SERVERLESS = True deprecation_filter: DeprecationFilter - @classmethod - async def _setup_class(cls): - await super()._setup_class() - cls.deprecation_filter = DeprecationFilter() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.deprecation_filter = DeprecationFilter() - @classmethod - async def _tearDown_class(cls): - cls.deprecation_filter.stop() - await super()._tearDown_class() + async def asyncTearDown(self) -> None: + self.deprecation_filter.stop() class TestRetryableWritesMMAPv1(IgnoreDeprecationsTest): knobs: client_knobs - @classmethod - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - cls.client = await cls.unmanaged_async_rs_or_single_client(retryWrites=True) - cls.db = cls.client.pymongo_test + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() + self.client = await self.async_rs_or_single_client(retryWrites=True) + self.db = self.client.pymongo_test - @classmethod - async def _tearDown_class(cls): - cls.knobs.disable() - await cls.client.close() - await super()._tearDown_class() + async def asyncTearDown(self) -> None: + self.knobs.disable() @async_client_context.require_no_standalone async def test_actionable_error_message(self): @@ -181,26 +174,18 @@ class TestRetryableWrites(IgnoreDeprecationsTest): listener: OvertCommandListener knobs: client_knobs - @classmethod @async_client_context.require_no_mmap - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self) -> None: + await super().asyncSetUp() # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - cls.listener = OvertCommandListener() - cls.client = await cls.unmanaged_async_rs_or_single_client( - retryWrites=True, event_listeners=[cls.listener] + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() + self.listener = OvertCommandListener() + self.client = await self.async_rs_or_single_client( + retryWrites=True, event_listeners=[self.listener] ) - cls.db = cls.client.pymongo_test + self.db = self.client.pymongo_test - @classmethod - async def _tearDown_class(cls): - cls.knobs.disable() - await cls.client.close() - await super()._tearDown_class() - - async def asyncSetUp(self): if async_client_context.is_rs and async_client_context.test_commands_enabled: await self.client.admin.command( SON([("configureFailPoint", "onPrimaryTransactionalWrite"), ("mode", "alwaysOn")]) @@ -211,6 +196,7 @@ async def asyncTearDown(self): await self.client.admin.command( SON([("configureFailPoint", "onPrimaryTransactionalWrite"), ("mode", "off")]) ) + self.knobs.disable() async def test_supported_single_statement_no_retry(self): listener = OvertCommandListener() @@ -480,13 +466,12 @@ class TestWriteConcernError(AsyncIntegrationTest): RUN_ON_SERVERLESS = True fail_insert: dict - @classmethod @async_client_context.require_replica_set @async_client_context.require_no_mmap @async_client_context.require_failCommand_fail_point - async def _setup_class(cls): - await super()._setup_class() - cls.fail_insert = { + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.fail_insert = { "configureFailPoint": "failCommand", "mode": {"times": 2}, "data": { diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index c1dac6f56d..e424796ce0 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -81,36 +81,27 @@ class TestSession(AsyncIntegrationTest): client2: AsyncMongoClient sensitive_commands: Set[str] - @classmethod @async_client_context.require_sessions - async def _setup_class(cls): - await super()._setup_class() + async def asyncSetUp(self): + await super().asyncSetUp() # Create a second client so we can make sure clients cannot share # sessions. - cls.client2 = await cls.unmanaged_async_rs_or_single_client() + self.client2 = await self.async_rs_or_single_client() # Redact no commands, so we can test user-admin commands have "lsid". - cls.sensitive_commands = monitoring._SENSITIVE_COMMANDS.copy() + self.sensitive_commands = monitoring._SENSITIVE_COMMANDS.copy() monitoring._SENSITIVE_COMMANDS.clear() - @classmethod - async def _tearDown_class(cls): - monitoring._SENSITIVE_COMMANDS.update(cls.sensitive_commands) - await cls.client2.close() - await super()._tearDown_class() - - async def asyncSetUp(self): self.listener = SessionTestListener() self.session_checker_listener = SessionTestListener() self.client = await self.async_rs_or_single_client( event_listeners=[self.listener, self.session_checker_listener] ) - self.addAsyncCleanup(self.client.close) self.db = self.client.pymongo_test self.initial_lsids = {s["id"] for s in session_ids(self.client)} async def asyncTearDown(self): - """All sessions used in the test must be returned to the pool.""" + monitoring._SENSITIVE_COMMANDS.update(self.sensitive_commands) await self.client.drop_database("pymongo_test") used_lsids = self.initial_lsids.copy() for event in self.session_checker_listener.started_events: @@ -120,6 +111,8 @@ async def asyncTearDown(self): current_lsids = {s["id"] for s in session_ids(self.client)} self.assertLessEqual(used_lsids, current_lsids) + await super().asyncTearDown() + async def _test_ops(self, client, *ops): listener = client.options.event_listeners[0] @@ -831,18 +824,11 @@ class TestCausalConsistency(AsyncUnitTest): listener: SessionTestListener client: AsyncMongoClient - @classmethod - async def _setup_class(cls): - cls.listener = SessionTestListener() - cls.client = await cls.unmanaged_async_rs_or_single_client(event_listeners=[cls.listener]) - - @classmethod - async def _tearDown_class(cls): - await cls.client.close() - @async_client_context.require_sessions async def asyncSetUp(self): await super().asyncSetUp() + self.listener = SessionTestListener() + self.client = await self.async_rs_or_single_client(event_listeners=[self.listener]) @async_client_context.require_no_standalone async def test_core(self): diff --git a/test/asynchronous/test_transactions.py b/test/asynchronous/test_transactions.py index 229046e79b..d11d0a9776 100644 --- a/test/asynchronous/test_transactions.py +++ b/test/asynchronous/test_transactions.py @@ -403,21 +403,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): class TestTransactionsConvenientAPI(AsyncTransactionsBase): - @classmethod - async def _setup_class(cls): - await super()._setup_class() - cls.mongos_clients = [] + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.mongos_clients = [] if async_client_context.supports_transactions(): for address in async_client_context.mongoses: - cls.mongos_clients.append( - await cls.unmanaged_async_single_client("{}:{}".format(*address)) - ) - - @classmethod - async def _tearDown_class(cls): - for client in cls.mongos_clients: - await client.close() - await super()._tearDown_class() + self.mongos_clients.append(await self.async_single_client("{}:{}".format(*address))) async def _set_fail_point(self, client, command_args): cmd = {"configureFailPoint": "failCommand"} diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 2ff38f06e9..f25e96e04d 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -479,54 +479,47 @@ async def insert_initial_data(self, initial_data): await db.create_collection(coll_name, write_concern=wc, **opts) @classmethod - async def _setup_class(cls): + def setUpClass(cls) -> None: + # Speed up the tests by decreasing the heartbeat frequency. + cls.knobs = client_knobs( + heartbeat_frequency=0.1, + min_heartbeat_interval=0.1, + kill_cursor_frequency=0.1, + events_queue_frequency=0.1, + ) + cls.knobs.enable() + + @classmethod + def tearDownClass(cls) -> None: + cls.knobs.disable() + + async def asyncSetUp(self): # super call creates internal client cls.client - await super()._setup_class() + await super().asyncSetUp() # process file-level runOnRequirements - run_on_spec = cls.TEST_SPEC.get("runOnRequirements", []) - if not await cls.should_run_on(run_on_spec): - raise unittest.SkipTest(f"{cls.__name__} runOnRequirements not satisfied") + run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) + if not await self.should_run_on(run_on_spec): + raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here if async_client_context.storage_engine == "mmapv1": - if "retryable-writes" in cls.TEST_SPEC["description"] or "retryable_writes" in str( - cls.TEST_PATH + if "retryable-writes" in self.TEST_SPEC["description"] or "retryable_writes" in str( + self.TEST_PATH ): raise unittest.SkipTest("MMAPv1 does not support retryWrites=True") # Handle mongos_clients for transactions tests. - cls.mongos_clients = [] + self.mongos_clients = [] if ( async_client_context.supports_transactions() and not async_client_context.load_balancer and not async_client_context.serverless ): for address in async_client_context.mongoses: - cls.mongos_clients.append( - await cls.unmanaged_async_single_client("{}:{}".format(*address)) - ) + self.mongos_clients.append(await self.async_single_client("{}:{}".format(*address))) - # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs( - heartbeat_frequency=0.1, - min_heartbeat_interval=0.1, - kill_cursor_frequency=0.1, - events_queue_frequency=0.1, - ) - cls.knobs.enable() - - @classmethod - async def _tearDown_class(cls): - cls.knobs.disable() - for client in cls.mongos_clients: - await client.close() - await super()._tearDown_class() - - async def asyncSetUp(self): - await super().asyncSetUp() # process schemaVersion # note: we check major schema version during class generation - # note: we do this here because we cannot run assertions in setUpClass version = Version.from_string(self.TEST_SPEC["schemaVersion"]) self.assertLessEqual( version, @@ -537,6 +530,11 @@ async def asyncSetUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) + async def asyncTearDown(self): + for client in self.mongos_clients: + await client.close() + await super().asyncTearDown() + def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if async_client_context.storage_engine == "mmapv1": diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index 4d9c4c8f20..f0463244d7 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -249,30 +249,24 @@ class AsyncSpecRunner(AsyncIntegrationTest): knobs: client_knobs listener: EventListener - @classmethod - async def _setup_class(cls): - await super()._setup_class() - cls.mongos_clients = [] + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + self.mongos_clients = [] # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - - @classmethod - async def _tearDown_class(cls): - cls.knobs.disable() - for client in cls.mongos_clients: - await client.close() - await super()._tearDown_class() - - def setUp(self): - super().setUp() + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() self.targets = {} self.listener = None # type: ignore self.pool_listener = None self.server_listener = None self.maxDiff = None + async def asyncTearDown(self) -> None: + self.knobs.disable() + for client in self.mongos_clients: + await client.close() + async def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) cmd.update(command_args) diff --git a/test/test_bulk.py b/test/test_bulk.py index 64fd48e8cd..ad22c1ce9a 100644 --- a/test/test_bulk.py +++ b/test/test_bulk.py @@ -42,15 +42,11 @@ class BulkTestBase(IntegrationTest): coll: Collection coll_w0: Collection - @classmethod - def _setup_class(cls): - super()._setup_class() - cls.coll = cls.db.test - cls.coll_w0 = cls.coll.with_options(write_concern=WriteConcern(w=0)) - def setUp(self): super().setUp() + self.coll = self.db.test self.coll.drop() + self.coll_w0 = self.coll.with_options(write_concern=WriteConcern(w=0)) def assertEqualResponse(self, expected, actual): """Compare response from bulk.execute() to expected response.""" @@ -785,12 +781,8 @@ def test_large_inserts_unordered(self): class BulkAuthorizationTestBase(BulkTestBase): - @classmethod @client_context.require_auth @client_context.require_no_api_version - def _setup_class(cls): - super()._setup_class() - def setUp(self): super().setUp() client_context.create_user(self.db.name, "readonly", "pw", ["read"]) @@ -935,21 +927,19 @@ class TestBulkWriteConcern(BulkTestBase): w: Optional[int] secondary: MongoClient - @classmethod - def _setup_class(cls): - super()._setup_class() - cls.w = client_context.w - cls.secondary = None - if cls.w is not None and cls.w > 1: + def setUp(self): + super().setUp() + self.w = client_context.w + self.secondary = None + if self.w is not None and self.w > 1: for member in (client_context.hello)["hosts"]: if member != (client_context.hello)["primary"]: - cls.secondary = cls.unmanaged_single_client(*partition_node(member)) + self.secondary = self.single_client(*partition_node(member)) break - @classmethod - def async_tearDownClass(cls): - if cls.secondary: - cls.secondary.close() + def tearDown(self): + if self.secondary: + self.secondary.close() def cause_wtimeout(self, requests, ordered): if not client_context.test_commands_enabled: diff --git a/test/test_change_stream.py b/test/test_change_stream.py index dae224c5e0..0742384184 100644 --- a/test/test_change_stream.py +++ b/test/test_change_stream.py @@ -819,18 +819,16 @@ def test_split_large_change(self): class TestClusterChangeStream(TestChangeStreamBase, APITestsMixin): dbs: list - @classmethod @client_context.require_version_min(4, 0, 0, -1) @client_context.require_change_streams - def _setup_class(cls): - super()._setup_class() - cls.dbs = [cls.db, cls.client.pymongo_test_2] + def setUp(self) -> None: + super().setUp() + self.dbs = [self.db, self.client.pymongo_test_2] - @classmethod - def _tearDown_class(cls): - for db in cls.dbs: - cls.client.drop_database(db) - super()._tearDown_class() + def tearDown(self): + for db in self.dbs: + self.client.drop_database(db) + super().tearDown() def change_stream_with_client(self, client, *args, **kwargs): return client.watch(*args, **kwargs) @@ -881,11 +879,10 @@ def test_full_pipeline(self): class TestDatabaseChangeStream(TestChangeStreamBase, APITestsMixin): - @classmethod @client_context.require_version_min(4, 0, 0, -1) @client_context.require_change_streams - def _setup_class(cls): - super()._setup_class() + def setUp(self) -> None: + super().setUp() def change_stream_with_client(self, client, *args, **kwargs): return client[self.db.name].watch(*args, **kwargs) @@ -967,12 +964,9 @@ def test_isolation(self): class TestCollectionChangeStream(TestChangeStreamBase, APITestsMixin, ProseSpecTestsMixin): - @classmethod @client_context.require_change_streams - def _setup_class(cls): - super()._setup_class() - def setUp(self): + super().setUp() # Use a new collection for each test. self.watched_collection().drop() self.watched_collection().insert_one({}) @@ -1110,20 +1104,11 @@ class TestAllLegacyScenarios(IntegrationTest): RUN_ON_LOAD_BALANCER = True listener: AllowListEventListener - @classmethod @client_context.require_connection - def _setup_class(cls): - super()._setup_class() - cls.listener = AllowListEventListener("aggregate", "getMore") - cls.client = cls.unmanaged_rs_or_single_client(event_listeners=[cls.listener]) - - @classmethod - def _tearDown_class(cls): - cls.client.close() - super()._tearDown_class() - def setUp(self): super().setUp() + self.listener = AllowListEventListener("aggregate", "getMore") + self.client = self.rs_or_single_client(event_listeners=[self.listener]) self.listener.reset() def setUpCluster(self, scenario_dict): diff --git a/test/test_client.py b/test/test_client.py index 07f3e560fe..d41b0bbfda 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -129,13 +129,8 @@ class ClientUnitTest(UnitTest): client: MongoClient - @classmethod - def _setup_class(cls): - cls.client = cls.unmanaged_rs_or_single_client(connect=False, serverSelectionTimeoutMS=100) - - @classmethod - def _tearDown_class(cls): - cls.client.close() + def setUp(self) -> None: + self.client = self.rs_or_single_client(connect=False, serverSelectionTimeoutMS=100) @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): diff --git a/test/test_collation.py b/test/test_collation.py index e5c1c7eb11..6d4e958a1f 100644 --- a/test/test_collation.py +++ b/test/test_collation.py @@ -97,26 +97,20 @@ class TestCollation(IntegrationTest): warn_context: Any collation: Collation - @classmethod @client_context.require_connection - def _setup_class(cls): - super()._setup_class() - cls.listener = EventListener() - cls.client = cls.unmanaged_rs_or_single_client(event_listeners=[cls.listener]) - cls.db = cls.client.pymongo_test - cls.collation = Collation("en_US") - cls.warn_context = warnings.catch_warnings() - cls.warn_context.__enter__() + def setUp(self) -> None: + super().setUp() + self.listener = EventListener() + self.client = self.rs_or_single_client(event_listeners=[self.listener]) + self.db = self.client.pymongo_test + self.collation = Collation("en_US") + self.warn_context = warnings.catch_warnings() + self.warn_context.__enter__() warnings.simplefilter("ignore", DeprecationWarning) - @classmethod - def _tearDown_class(cls): - cls.warn_context.__exit__() - cls.warn_context = None - cls.client.close() - super()._tearDown_class() - - def tearDown(self): + def tearDown(self) -> None: + self.warn_context.__exit__() + self.warn_context = None self.listener.reset() super().tearDown() diff --git a/test/test_collection.py b/test/test_collection.py index f2f01ac686..9364d34e34 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -86,14 +86,10 @@ class TestCollectionNoConnect(UnitTest): db: Database client: MongoClient - @classmethod - def _setup_class(cls): - cls.client = MongoClient(connect=False) - cls.db = cls.client.pymongo_test - - @classmethod - def _tearDown_class(cls): - cls.client.close() + def setUp(self) -> None: + super().setUp() + self.client = self.simple_client(connect=False) + self.db = self.client.pymongo_test def test_collection(self): self.assertRaises(TypeError, Collection, self.db, 5) @@ -163,27 +159,14 @@ def test_iteration(self): class TestCollection(IntegrationTest): w: int - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.w = client_context.w # type: ignore - - @classmethod - def tearDownClass(cls): - if _IS_SYNC: - cls.db.drop_collection("test_large_limit") # type: ignore[unused-coroutine] - else: - asyncio.run(cls.async_tearDownClass()) - - @classmethod - def async_tearDownClass(cls): - cls.db.drop_collection("test_large_limit") - def setUp(self): - self.db.test.drop() + super().setUp() + self.w = client_context.w # type: ignore def tearDown(self): self.db.test.drop() + self.db.drop_collection("test_large_limit") + super().tearDown() @contextlib.contextmanager def write_concern_collection(self): diff --git a/test/test_connections_survive_primary_stepdown_spec.py b/test/test_connections_survive_primary_stepdown_spec.py index 984d700fb3..4387850a00 100644 --- a/test/test_connections_survive_primary_stepdown_spec.py +++ b/test/test_connections_survive_primary_stepdown_spec.py @@ -44,30 +44,22 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest): listener: CMAPListener coll: Collection - @classmethod + def tearDown(self): + reset_client_context() + @client_context.require_replica_set - def _setup_class(cls): - super()._setup_class() - cls.listener = CMAPListener() - cls.client = cls.unmanaged_rs_or_single_client( - event_listeners=[cls.listener], retryWrites=False, heartbeatFrequencyMS=500 + def setUp(self): + self.listener = CMAPListener() + self.client = self.rs_or_single_client( + event_listeners=[self.listener], retryWrites=False, heartbeatFrequencyMS=500 ) # Ensure connections to all servers in replica set. This is to test # that the is_writable flag is properly updated for connections that # survive a replica set election. - ensure_all_connected(cls.client) - cls.listener.reset() - - cls.db = cls.client.get_database("step-down", write_concern=WriteConcern("majority")) - cls.coll = cls.db.get_collection("step-down", write_concern=WriteConcern("majority")) - - @classmethod - def _tearDown_class(cls): - cls.client.close() - reset_client_context() - - def setUp(self): + ensure_all_connected(self.client) + self.db = self.client.get_database("step-down", write_concern=WriteConcern("majority")) + self.coll = self.db.get_collection("step-down", write_concern=WriteConcern("majority")) # Note that all ops use same write-concern as self.db (majority). self.db.drop_collection("step-down") self.db.create_collection("step-down") diff --git a/test/test_cursor.py b/test/test_cursor.py index 7a6dfc9429..e687abcfbf 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -1636,10 +1636,6 @@ def test_monitoring(self): class TestRawBatchCommandCursor(IntegrationTest): - @classmethod - def _setup_class(cls): - super()._setup_class() - def test_aggregate_raw(self): c = self.db.test c.drop() diff --git a/test/test_custom_types.py b/test/test_custom_types.py index abaa820cb7..6771ea25f9 100644 --- a/test/test_custom_types.py +++ b/test/test_custom_types.py @@ -633,6 +633,7 @@ class MyType(pytype): # type: ignore class TestCollectionWCustomType(IntegrationTest): def setUp(self): + super().setUp() self.db.test.drop() def tearDown(self): @@ -754,6 +755,7 @@ def test_find_one_and__w_custom_type_decoder(self): class TestGridFileCustomType(IntegrationTest): def setUp(self): + super().setUp() self.db.drop_collection("fs.files") self.db.drop_collection("fs.chunks") @@ -917,11 +919,10 @@ def run_test(doc_cls): class TestCollectionChangeStreamsWCustomTypes(IntegrationTest, ChangeStreamsWCustomTypesTestMixin): - @classmethod @client_context.require_change_streams - def setUpClass(cls): - super().setUpClass() - cls.db.test.delete_many({}) + def setUp(self): + super().setUp() + self.db.test.delete_many({}) def tearDown(self): self.input_target.drop() @@ -935,12 +936,11 @@ def create_targets(self, *args, **kwargs): class TestDatabaseChangeStreamsWCustomTypes(IntegrationTest, ChangeStreamsWCustomTypesTestMixin): - @classmethod @client_context.require_version_min(4, 0, 0) @client_context.require_change_streams - def setUpClass(cls): - super().setUpClass() - cls.db.test.delete_many({}) + def setUp(self): + super().setUp() + self.db.test.delete_many({}) def tearDown(self): self.input_target.drop() @@ -954,12 +954,11 @@ def create_targets(self, *args, **kwargs): class TestClusterChangeStreamsWCustomTypes(IntegrationTest, ChangeStreamsWCustomTypesTestMixin): - @classmethod @client_context.require_version_min(4, 0, 0) @client_context.require_change_streams - def setUpClass(cls): - super().setUpClass() - cls.db.test.delete_many({}) + def setUp(self): + super().setUp() + self.db.test.delete_many({}) def tearDown(self): self.input_target.drop() diff --git a/test/test_database.py b/test/test_database.py index 4973ed0134..5e854c941d 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -709,6 +709,7 @@ def test_with_options(self): class TestDatabaseAggregation(IntegrationTest): def setUp(self): + super().setUp() self.pipeline: List[Mapping[str, Any]] = [ {"$listLocalSessions": {}}, {"$limit": 1}, diff --git a/test/test_encryption.py b/test/test_encryption.py index 13a69ca9ad..3749354217 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -211,11 +211,10 @@ def test_kwargs(self): class EncryptionIntegrationTest(IntegrationTest): """Base class for encryption integration tests.""" - @classmethod @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") @client_context.require_version_min(4, 2, -1) - def _setup_class(cls): - super()._setup_class() + def setUp(self) -> None: + super().setUp() def assertEncrypted(self, val): self.assertIsInstance(val, Binary) @@ -430,10 +429,9 @@ def test_upsert_uuid_standard_encrypt(self): class TestClientMaxWireVersion(IntegrationTest): - @classmethod @unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed") - def _setup_class(cls): - super()._setup_class() + def setUp(self): + super().setUp() @client_context.require_version_max(4, 0, 99) def test_raise_max_wire_version_error(self): @@ -816,17 +814,16 @@ class TestDataKeyDoubleEncryption(EncryptionIntegrationTest): "local": None, } - @classmethod @unittest.skipUnless( any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]), "No environment credentials are set", ) - def _setup_class(cls): - super()._setup_class() - cls.listener = OvertCommandListener() - cls.client = cls.unmanaged_rs_or_single_client(event_listeners=[cls.listener]) - cls.client.db.coll.drop() - cls.vault = create_key_vault(cls.client.keyvault.datakeys) + def setUp(self): + super().setUp() + self.listener = OvertCommandListener() + self.client = self.rs_or_single_client(event_listeners=[self.listener]) + self.client.db.coll.drop() + self.vault = create_key_vault(self.client.keyvault.datakeys) # Configure the encrypted field via the local schema_map option. schemas = { @@ -844,25 +841,22 @@ def _setup_class(cls): } } opts = AutoEncryptionOpts( - cls.KMS_PROVIDERS, "keyvault.datakeys", schema_map=schemas, kms_tls_options=KMS_TLS_OPTS + self.KMS_PROVIDERS, + "keyvault.datakeys", + schema_map=schemas, + kms_tls_options=KMS_TLS_OPTS, ) - cls.client_encrypted = cls.unmanaged_rs_or_single_client( + self.client_encrypted = self.rs_or_single_client( auto_encryption_opts=opts, uuidRepresentation="standard" ) - cls.client_encryption = cls.unmanaged_create_client_encryption( - cls.KMS_PROVIDERS, "keyvault.datakeys", cls.client, OPTS, kms_tls_options=KMS_TLS_OPTS + self.client_encryption = self.create_client_encryption( + self.KMS_PROVIDERS, "keyvault.datakeys", self.client, OPTS, kms_tls_options=KMS_TLS_OPTS ) - - @classmethod - def _tearDown_class(cls): - cls.vault.drop() - cls.client.close() - cls.client_encrypted.close() - cls.client_encryption.close() - - def setUp(self): self.listener.reset() + def tearDown(self) -> None: + self.vault.drop() + def run_test(self, provider_name): # Create data key. master_key: Any = self.MASTER_KEYS[provider_name] @@ -1007,10 +1001,9 @@ def test_views_are_prohibited(self): class TestCorpus(EncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set") - def _setup_class(cls): - super()._setup_class() + def setUp(self): + super().setUp() @staticmethod def kms_providers(): @@ -1184,12 +1177,11 @@ class TestBsonSizeBatches(EncryptionIntegrationTest): client_encrypted: MongoClient listener: OvertCommandListener - @classmethod - def _setup_class(cls): - super()._setup_class() + def setUp(self): + super().setUp() db = client_context.client.db - cls.coll = db.coll - cls.coll.drop() + self.coll = db.coll + self.coll.drop() # Configure the encrypted 'db.coll' collection via jsonSchema. json_schema = json_data("limits", "limits-schema.json") db.create_collection( @@ -1207,17 +1199,14 @@ def _setup_class(cls): coll.insert_one(json_data("limits", "limits-key.json")) opts = AutoEncryptionOpts({"local": {"key": LOCAL_MASTER_KEY}}, "keyvault.datakeys") - cls.listener = OvertCommandListener() - cls.client_encrypted = cls.unmanaged_rs_or_single_client( - auto_encryption_opts=opts, event_listeners=[cls.listener] + self.listener = OvertCommandListener() + self.client_encrypted = self.rs_or_single_client( + auto_encryption_opts=opts, event_listeners=[self.listener] ) - cls.coll_encrypted = cls.client_encrypted.db.coll + self.coll_encrypted = self.client_encrypted.db.coll - @classmethod - def _tearDown_class(cls): - cls.coll_encrypted.drop() - cls.client_encrypted.close() - super()._tearDown_class() + def tearDown(self) -> None: + self.coll_encrypted.drop() def test_01_insert_succeeds_under_2MiB(self): doc = {"_id": "over_2mib_under_16mib", "unencrypted": "a" * _2_MiB} @@ -1281,15 +1270,12 @@ def test_06_insert_fails_over_16MiB(self): class TestCustomEndpoint(EncryptionIntegrationTest): """Prose tests for creating data keys with a custom endpoint.""" - @classmethod @unittest.skipUnless( any([all(AWS_CREDS.values()), all(AZURE_CREDS.values()), all(GCP_CREDS.values())]), "No environment credentials are set", ) - def _setup_class(cls): - super()._setup_class() - def setUp(self): + super().setUp() kms_providers = { "aws": AWS_CREDS, "azure": AZURE_CREDS, @@ -1318,10 +1304,6 @@ def setUp(self): self._kmip_host_error = None self._invalid_host_error = None - def tearDown(self): - self.client_encryption.close() - self.client_encryption_invalid.close() - def run_test_expected_success(self, provider_name, master_key): data_key_id = self.client_encryption.create_data_key(provider_name, master_key=master_key) encrypted = self.client_encryption.encrypt( @@ -1495,6 +1477,7 @@ class AzureGCPEncryptionTestMixin(EncryptionIntegrationTest): client: MongoClient def setUp(self): + self.client = self.simple_client() keyvault = self.client.get_database(self.KEYVAULT_DB).get_collection(self.KEYVAULT_COLL) create_key_vault(keyvault, self.DEK) @@ -1553,13 +1536,12 @@ def _test_automatic(self, expectation_extjson, payload): class TestAzureEncryption(AzureGCPEncryptionTestMixin, EncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(AZURE_CREDS.values()), "Azure environment credentials are not set") - def _setup_class(cls): - cls.KMS_PROVIDER_MAP = {"azure": AZURE_CREDS} - cls.DEK = json_data(BASE, "custom", "azure-dek.json") - cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") - super()._setup_class() + def setUp(self): + self.KMS_PROVIDER_MAP = {"azure": AZURE_CREDS} + self.DEK = json_data(BASE, "custom", "azure-dek.json") + self.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") + super().setUp() def test_explicit(self): return self._test_explicit( @@ -1579,13 +1561,12 @@ def test_automatic(self): class TestGCPEncryption(AzureGCPEncryptionTestMixin, EncryptionIntegrationTest): - @classmethod @unittest.skipUnless(any(GCP_CREDS.values()), "GCP environment credentials are not set") - def _setup_class(cls): - cls.KMS_PROVIDER_MAP = {"gcp": GCP_CREDS} - cls.DEK = json_data(BASE, "custom", "gcp-dek.json") - cls.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") - super()._setup_class() + def setUp(self): + self.KMS_PROVIDER_MAP = {"gcp": GCP_CREDS} + self.DEK = json_data(BASE, "custom", "gcp-dek.json") + self.SCHEMA_MAP = json_data(BASE, "custom", "azure-gcp-schema.json") + super().setUp() def test_explicit(self): return self._test_explicit( @@ -3071,17 +3052,11 @@ class TestNoSessionsSupport(EncryptionIntegrationTest): mongocryptd_client: MongoClient MONGOCRYPTD_PORT = 27020 - @classmethod @unittest.skipIf(os.environ.get("TEST_CRYPT_SHARED"), "crypt_shared lib is installed") - def _setup_class(cls): - super()._setup_class() - start_mongocryptd(cls.MONGOCRYPTD_PORT) - - @classmethod - def _tearDown_class(cls): - super()._tearDown_class() - def setUp(self) -> None: + super().setUp() + start_mongocryptd(self.MONGOCRYPTD_PORT) + self.listener = OvertCommandListener() self.mongocryptd_client = self.simple_client( f"mongodb://localhost:{self.MONGOCRYPTD_PORT}", event_listeners=[self.listener] diff --git a/test/test_examples.py b/test/test_examples.py index ebf1d784a3..7f98226e7a 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -33,19 +33,14 @@ class TestSampleShellCommands(IntegrationTest): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Run once before any tests run. - cls.db.inventory.drop() - - @classmethod - def tearDownClass(cls): - cls.client.drop_database("pymongo_test") + def setUp(self): + super().setUp() + self.db.inventory.drop() def tearDown(self): # Run after every test. self.db.inventory.drop() + self.client.drop_database("pymongo_test") def test_first_three_examples(self): db = self.db diff --git a/test/test_grid_file.py b/test/test_grid_file.py index fe88aec5ff..0a5b1ad40a 100644 --- a/test/test_grid_file.py +++ b/test/test_grid_file.py @@ -97,6 +97,7 @@ def test_grid_in_custom_opts(self): class TestGridFile(IntegrationTest): def setUp(self): + super().setUp() self.cleanup_colls(self.db.fs.files, self.db.fs.chunks) def test_basic(self): diff --git a/test/test_gridfs.py b/test/test_gridfs.py index 549dc0b204..a36109f399 100644 --- a/test/test_gridfs.py +++ b/test/test_gridfs.py @@ -75,9 +75,9 @@ def run(self): class TestGridfsNoConnect(unittest.TestCase): db: Database - @classmethod - def setUpClass(cls): - cls.db = MongoClient(connect=False).pymongo_test + def setUp(self): + super().setUp() + self.db = MongoClient(connect=False).pymongo_test def test_gridfs(self): self.assertRaises(TypeError, gridfs.GridFS, "foo") @@ -88,13 +88,10 @@ class TestGridfs(IntegrationTest): fs: gridfs.GridFS alt: gridfs.GridFS - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.fs = gridfs.GridFS(cls.db) - cls.alt = gridfs.GridFS(cls.db, "alt") - def setUp(self): + super().setUp() + self.fs = gridfs.GridFS(self.db) + self.alt = gridfs.GridFS(self.db, "alt") self.cleanup_colls( self.db.fs.files, self.db.fs.chunks, self.db.alt.files, self.db.alt.chunks ) @@ -509,10 +506,9 @@ def test_md5(self): class TestGridfsReplicaSet(IntegrationTest): - @classmethod @client_context.require_secondaries_count(1) - def setUpClass(cls): - super().setUpClass() + def setUp(self): + super().setUp() @classmethod def tearDownClass(cls): diff --git a/test/test_gridfs_bucket.py b/test/test_gridfs_bucket.py index 28adb7051a..04c7427350 100644 --- a/test/test_gridfs_bucket.py +++ b/test/test_gridfs_bucket.py @@ -79,13 +79,10 @@ class TestGridfs(IntegrationTest): fs: gridfs.GridFSBucket alt: gridfs.GridFSBucket - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.fs = gridfs.GridFSBucket(cls.db) - cls.alt = gridfs.GridFSBucket(cls.db, bucket_name="alt") - def setUp(self): + super().setUp() + self.fs = gridfs.GridFSBucket(self.db) + self.alt = gridfs.GridFSBucket(self.db, bucket_name="alt") self.cleanup_colls( self.db.fs.files, self.db.fs.chunks, self.db.alt.files, self.db.alt.chunks ) @@ -479,10 +476,9 @@ def test_md5(self): class TestGridfsBucketReplicaSet(IntegrationTest): - @classmethod @client_context.require_secondaries_count(1) - def setUpClass(cls): - super().setUpClass() + def setUp(self): + super().setUp() @classmethod def tearDownClass(cls): diff --git a/test/test_monitoring.py b/test/test_monitoring.py index a0c520ed27..31f546fe54 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -51,22 +51,14 @@ class TestCommandMonitoring(IntegrationTest): listener: EventListener @classmethod - @client_context.require_connection - def _setup_class(cls): - super()._setup_class() + def setUpClass(cls) -> None: cls.listener = EventListener() - cls.client = cls.unmanaged_rs_or_single_client( - event_listeners=[cls.listener], retryWrites=False - ) - @classmethod - def _tearDown_class(cls): - cls.client.close() - super()._tearDown_class() - - def tearDown(self): + @client_context.require_connection + def setUp(self) -> None: + super().setUp() self.listener.reset() - super().tearDown() + self.client = self.rs_or_single_client(event_listeners=[self.listener], retryWrites=False) def test_started_simple(self): self.client.pymongo_test.command("ping") @@ -1137,27 +1129,30 @@ class TestGlobalListener(IntegrationTest): saved_listeners: Any @classmethod - @client_context.require_connection - def _setup_class(cls): - super()._setup_class() + def setUpClass(cls) -> None: cls.listener = EventListener() # We plan to call register(), which internally modifies _LISTENERS. cls.saved_listeners = copy.deepcopy(monitoring._LISTENERS) monitoring.register(cls.listener) - cls.client = cls.unmanaged_single_client() - # Get one (authenticated) socket in the pool. - cls.client.pymongo_test.command("ping") - - @classmethod - def _tearDown_class(cls): - monitoring._LISTENERS = cls.saved_listeners - cls.client.close() - super()._tearDown_class() + @client_context.require_connection def setUp(self): super().setUp() + self.listener = EventListener() + # We plan to call register(), which internally modifies _LISTENERS. + self.saved_listeners = copy.deepcopy(monitoring._LISTENERS) + monitoring.register(self.listener) + self.client = self.single_client() + # Get one (authenticated) socket in the pool. + self.client.pymongo_test.command("ping") + + def tearDown(self) -> None: self.listener.reset() + @classmethod + def tearDownClass(cls): + monitoring._LISTENERS = cls.saved_listeners + def test_simple(self): self.client.pymongo_test.command("ping") started = self.listener.started_events[0] diff --git a/test/test_read_concern.py b/test/test_read_concern.py index ea9ce49a30..f7c0901422 100644 --- a/test/test_read_concern.py +++ b/test/test_read_concern.py @@ -31,24 +31,16 @@ class TestReadConcern(IntegrationTest): listener: OvertCommandListener - @classmethod @client_context.require_connection - def setUpClass(cls): - super().setUpClass() - cls.listener = OvertCommandListener() - cls.client = cls.unmanaged_rs_or_single_client(event_listeners=[cls.listener]) - cls.db = cls.client.pymongo_test + def setUp(self): + super().setUp() + self.listener = OvertCommandListener() + self.client = self.rs_or_single_client(event_listeners=[self.listener]) + self.db = self.client.pymongo_test client_context.client.pymongo_test.create_collection("coll") - @classmethod - def tearDownClass(cls): - cls.client.close() - client_context.client.pymongo_test.drop_collection("coll") - super().tearDownClass() - def tearDown(self): - self.listener.reset() - super().tearDown() + client_context.client.pymongo_test.drop_collection("coll") def test_read_concern(self): rc = ReadConcern() diff --git a/test/test_retryable_writes.py b/test/test_retryable_writes.py index 5df6c41f7a..eb814c4ef9 100644 --- a/test/test_retryable_writes.py +++ b/test/test_retryable_writes.py @@ -133,34 +133,27 @@ class IgnoreDeprecationsTest(IntegrationTest): RUN_ON_SERVERLESS = True deprecation_filter: DeprecationFilter - @classmethod - def _setup_class(cls): - super()._setup_class() - cls.deprecation_filter = DeprecationFilter() + def setUp(self) -> None: + super().setUp() + self.deprecation_filter = DeprecationFilter() - @classmethod - def _tearDown_class(cls): - cls.deprecation_filter.stop() - super()._tearDown_class() + def tearDown(self) -> None: + self.deprecation_filter.stop() class TestRetryableWritesMMAPv1(IgnoreDeprecationsTest): knobs: client_knobs - @classmethod - def _setup_class(cls): - super()._setup_class() + def setUp(self) -> None: + super().setUp() # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - cls.client = cls.unmanaged_rs_or_single_client(retryWrites=True) - cls.db = cls.client.pymongo_test + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() + self.client = self.rs_or_single_client(retryWrites=True) + self.db = self.client.pymongo_test - @classmethod - def _tearDown_class(cls): - cls.knobs.disable() - cls.client.close() - super()._tearDown_class() + def tearDown(self) -> None: + self.knobs.disable() @client_context.require_no_standalone def test_actionable_error_message(self): @@ -181,26 +174,16 @@ class TestRetryableWrites(IgnoreDeprecationsTest): listener: OvertCommandListener knobs: client_knobs - @classmethod @client_context.require_no_mmap - def _setup_class(cls): - super()._setup_class() + def setUp(self) -> None: + super().setUp() # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - cls.listener = OvertCommandListener() - cls.client = cls.unmanaged_rs_or_single_client( - retryWrites=True, event_listeners=[cls.listener] - ) - cls.db = cls.client.pymongo_test - - @classmethod - def _tearDown_class(cls): - cls.knobs.disable() - cls.client.close() - super()._tearDown_class() + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() + self.listener = OvertCommandListener() + self.client = self.rs_or_single_client(retryWrites=True, event_listeners=[self.listener]) + self.db = self.client.pymongo_test - def setUp(self): if client_context.is_rs and client_context.test_commands_enabled: self.client.admin.command( SON([("configureFailPoint", "onPrimaryTransactionalWrite"), ("mode", "alwaysOn")]) @@ -211,6 +194,7 @@ def tearDown(self): self.client.admin.command( SON([("configureFailPoint", "onPrimaryTransactionalWrite"), ("mode", "off")]) ) + self.knobs.disable() def test_supported_single_statement_no_retry(self): listener = OvertCommandListener() @@ -480,13 +464,12 @@ class TestWriteConcernError(IntegrationTest): RUN_ON_SERVERLESS = True fail_insert: dict - @classmethod @client_context.require_replica_set @client_context.require_no_mmap @client_context.require_failCommand_fail_point - def _setup_class(cls): - super()._setup_class() - cls.fail_insert = { + def setUp(self) -> None: + super().setUp() + self.fail_insert = { "configureFailPoint": "failCommand", "mode": {"times": 2}, "data": { diff --git a/test/test_sdam_monitoring_spec.py b/test/test_sdam_monitoring_spec.py index 81b208d511..6b808b159d 100644 --- a/test/test_sdam_monitoring_spec.py +++ b/test/test_sdam_monitoring_spec.py @@ -270,7 +270,7 @@ class TestSdamMonitoring(IntegrationTest): @classmethod @client_context.require_failCommand_fail_point def setUpClass(cls): - super().setUpClass() + super().setUp(cls) # Speed up the tests by decreasing the event publish frequency. cls.knobs = client_knobs( events_queue_frequency=0.1, heartbeat_frequency=0.1, min_heartbeat_interval=0.1 diff --git a/test/test_session.py b/test/test_session.py index 9f94ded927..980d9df688 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -81,36 +81,27 @@ class TestSession(IntegrationTest): client2: MongoClient sensitive_commands: Set[str] - @classmethod @client_context.require_sessions - def _setup_class(cls): - super()._setup_class() + def setUp(self): + super().setUp() # Create a second client so we can make sure clients cannot share # sessions. - cls.client2 = cls.unmanaged_rs_or_single_client() + self.client2 = self.rs_or_single_client() # Redact no commands, so we can test user-admin commands have "lsid". - cls.sensitive_commands = monitoring._SENSITIVE_COMMANDS.copy() + self.sensitive_commands = monitoring._SENSITIVE_COMMANDS.copy() monitoring._SENSITIVE_COMMANDS.clear() - @classmethod - def _tearDown_class(cls): - monitoring._SENSITIVE_COMMANDS.update(cls.sensitive_commands) - cls.client2.close() - super()._tearDown_class() - - def setUp(self): self.listener = SessionTestListener() self.session_checker_listener = SessionTestListener() self.client = self.rs_or_single_client( event_listeners=[self.listener, self.session_checker_listener] ) - self.addCleanup(self.client.close) self.db = self.client.pymongo_test self.initial_lsids = {s["id"] for s in session_ids(self.client)} def tearDown(self): - """All sessions used in the test must be returned to the pool.""" + monitoring._SENSITIVE_COMMANDS.update(self.sensitive_commands) self.client.drop_database("pymongo_test") used_lsids = self.initial_lsids.copy() for event in self.session_checker_listener.started_events: @@ -120,6 +111,8 @@ def tearDown(self): current_lsids = {s["id"] for s in session_ids(self.client)} self.assertLessEqual(used_lsids, current_lsids) + super().tearDown() + def _test_ops(self, client, *ops): listener = client.options.event_listeners[0] @@ -831,18 +824,11 @@ class TestCausalConsistency(UnitTest): listener: SessionTestListener client: MongoClient - @classmethod - def _setup_class(cls): - cls.listener = SessionTestListener() - cls.client = cls.unmanaged_rs_or_single_client(event_listeners=[cls.listener]) - - @classmethod - def _tearDown_class(cls): - cls.client.close() - @client_context.require_sessions def setUp(self): super().setUp() + self.listener = SessionTestListener() + self.client = self.rs_or_single_client(event_listeners=[self.listener]) @client_context.require_no_standalone def test_core(self): diff --git a/test/test_threads.py b/test/test_threads.py index b3dadbb1a3..3e469e28fe 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -105,6 +105,7 @@ def run(self): class TestThreads(IntegrationTest): def setUp(self): + super().setUp() self.db = self.client.pymongo_test def test_threading(self): diff --git a/test/test_transactions.py b/test/test_transactions.py index 3cecbe9d38..949b88e60b 100644 --- a/test/test_transactions.py +++ b/test/test_transactions.py @@ -395,19 +395,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): class TestTransactionsConvenientAPI(TransactionsBase): - @classmethod - def _setup_class(cls): - super()._setup_class() - cls.mongos_clients = [] + def setUp(self) -> None: + super().setUp() + self.mongos_clients = [] if client_context.supports_transactions(): for address in client_context.mongoses: - cls.mongos_clients.append(cls.unmanaged_single_client("{}:{}".format(*address))) - - @classmethod - def _tearDown_class(cls): - for client in cls.mongos_clients: - client.close() - super()._tearDown_class() + self.mongos_clients.append(self.single_client("{}:{}".format(*address))) def _set_fail_point(self, client, command_args): cmd = {"configureFailPoint": "failCommand"} diff --git a/test/test_typing.py b/test/test_typing.py index 441707616e..bfe4d032c1 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -114,10 +114,9 @@ def test_mypy_failures(self) -> None: class TestPymongo(IntegrationTest): coll: Collection - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.coll = cls.client.test.test + def setUp(self): + super().setUp() + self.coll = self.client.test.test def test_insert_find(self) -> None: doc = {"my": "doc"} diff --git a/test/unified_format.py b/test/unified_format.py index 13ab0af69b..7d5c4e4e03 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -478,52 +478,47 @@ def insert_initial_data(self, initial_data): db.create_collection(coll_name, write_concern=wc, **opts) @classmethod - def _setup_class(cls): + def setUpClass(cls) -> None: + # Speed up the tests by decreasing the heartbeat frequency. + cls.knobs = client_knobs( + heartbeat_frequency=0.1, + min_heartbeat_interval=0.1, + kill_cursor_frequency=0.1, + events_queue_frequency=0.1, + ) + cls.knobs.enable() + + @classmethod + def tearDownClass(cls) -> None: + cls.knobs.disable() + + def setUp(self): # super call creates internal client cls.client - super()._setup_class() + super().setUp() # process file-level runOnRequirements - run_on_spec = cls.TEST_SPEC.get("runOnRequirements", []) - if not cls.should_run_on(run_on_spec): - raise unittest.SkipTest(f"{cls.__name__} runOnRequirements not satisfied") + run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) + if not self.should_run_on(run_on_spec): + raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here if client_context.storage_engine == "mmapv1": - if "retryable-writes" in cls.TEST_SPEC["description"] or "retryable_writes" in str( - cls.TEST_PATH + if "retryable-writes" in self.TEST_SPEC["description"] or "retryable_writes" in str( + self.TEST_PATH ): raise unittest.SkipTest("MMAPv1 does not support retryWrites=True") # Handle mongos_clients for transactions tests. - cls.mongos_clients = [] + self.mongos_clients = [] if ( client_context.supports_transactions() and not client_context.load_balancer and not client_context.serverless ): for address in client_context.mongoses: - cls.mongos_clients.append(cls.unmanaged_single_client("{}:{}".format(*address))) - - # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs( - heartbeat_frequency=0.1, - min_heartbeat_interval=0.1, - kill_cursor_frequency=0.1, - events_queue_frequency=0.1, - ) - cls.knobs.enable() + self.mongos_clients.append(self.single_client("{}:{}".format(*address))) - @classmethod - def _tearDown_class(cls): - cls.knobs.disable() - for client in cls.mongos_clients: - client.close() - super()._tearDown_class() - - def setUp(self): - super().setUp() # process schemaVersion # note: we check major schema version during class generation - # note: we do this here because we cannot run assertions in setUpClass version = Version.from_string(self.TEST_SPEC["schemaVersion"]) self.assertLessEqual( version, @@ -534,6 +529,11 @@ def setUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) + def tearDown(self): + for client in self.mongos_clients: + client.close() + super().tearDown() + def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if client_context.storage_engine == "mmapv1": diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index 8a061de0b1..682cf0b0f8 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -249,30 +249,24 @@ class SpecRunner(IntegrationTest): knobs: client_knobs listener: EventListener - @classmethod - def _setup_class(cls): - super()._setup_class() - cls.mongos_clients = [] + def setUp(self) -> None: + super().setUp() + self.mongos_clients = [] # Speed up the tests by decreasing the heartbeat frequency. - cls.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) - cls.knobs.enable() - - @classmethod - def _tearDown_class(cls): - cls.knobs.disable() - for client in cls.mongos_clients: - client.close() - super()._tearDown_class() - - def setUp(self): - super().setUp() + self.knobs = client_knobs(heartbeat_frequency=0.1, min_heartbeat_interval=0.1) + self.knobs.enable() self.targets = {} self.listener = None # type: ignore self.pool_listener = None self.server_listener = None self.maxDiff = None + def tearDown(self) -> None: + self.knobs.disable() + for client in self.mongos_clients: + client.close() + def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) cmd.update(command_args) From a23003f5a6b43685efcc4dc2605ebb0ab7af8ae6 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 17 Oct 2024 09:03:27 -0400 Subject: [PATCH 05/14] PYTHON-4860 - Async client should use asyncio.Lock and asyncio.Condition (#1934) --- THIRD-PARTY-NOTICES | 59 ++ pymongo/_asyncio_lock.py | 309 ++++++++++ pymongo/asynchronous/cursor.py | 4 +- pymongo/asynchronous/mongo_client.py | 12 +- pymongo/asynchronous/pool.py | 26 +- pymongo/asynchronous/topology.py | 17 +- pymongo/lock.py | 245 ++------ pymongo/synchronous/cursor.py | 2 +- pymongo/synchronous/mongo_client.py | 10 +- pymongo/synchronous/pool.py | 26 +- pymongo/synchronous/topology.py | 17 +- test/__init__.py | 2 +- test/asynchronous/__init__.py | 2 +- test/asynchronous/test_locks.py | 817 +++++++++++++-------------- tools/synchro.py | 35 +- 15 files changed, 867 insertions(+), 716 deletions(-) create mode 100644 pymongo/_asyncio_lock.py diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 0b9fc738ed..7e20a6f2bd 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -71,3 +71,62 @@ OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +3) License Notice for async_lock.py +----------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved" +are retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/pymongo/_asyncio_lock.py b/pymongo/_asyncio_lock.py new file mode 100644 index 0000000000..669b0f63a7 --- /dev/null +++ b/pymongo/_asyncio_lock.py @@ -0,0 +1,309 @@ +# Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved + +"""Lock and Condition classes vendored from https://github.com/python/cpython/blob/main/Lib/asyncio/locks.py +to port 3.13 fixes to older versions of Python. +Can be removed once we drop Python 3.12 support.""" + +from __future__ import annotations + +import collections +import threading +from asyncio import events, exceptions +from typing import Any, Coroutine, Optional + +_global_lock = threading.Lock() + + +class _LoopBoundMixin: + _loop = None + + def _get_loop(self) -> Any: + loop = events._get_running_loop() + + if self._loop is None: + with _global_lock: + if self._loop is None: + self._loop = loop + if loop is not self._loop: + raise RuntimeError(f"{self!r} is bound to a different event loop") + return loop + + +class _ContextManagerMixin: + async def __aenter__(self) -> None: + await self.acquire() # type: ignore[attr-defined] + # We have no use for the "as ..." clause in the with + # statement for locks. + return + + async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: + self.release() # type: ignore[attr-defined] + + +class Lock(_ContextManagerMixin, _LoopBoundMixin): + """Primitive lock objects. + + A primitive lock is a synchronization primitive that is not owned + by a particular task when locked. A primitive lock is in one + of two states, 'locked' or 'unlocked'. + + It is created in the unlocked state. It has two basic methods, + acquire() and release(). When the state is unlocked, acquire() + changes the state to locked and returns immediately. When the + state is locked, acquire() blocks until a call to release() in + another task changes it to unlocked, then the acquire() call + resets it to locked and returns. The release() method should only + be called in the locked state; it changes the state to unlocked + and returns immediately. If an attempt is made to release an + unlocked lock, a RuntimeError will be raised. + + When more than one task is blocked in acquire() waiting for + the state to turn to unlocked, only one task proceeds when a + release() call resets the state to unlocked; successive release() + calls will unblock tasks in FIFO order. + + Locks also support the asynchronous context management protocol. + 'async with lock' statement should be used. + + Usage: + + lock = Lock() + ... + await lock.acquire() + try: + ... + finally: + lock.release() + + Context manager usage: + + lock = Lock() + ... + async with lock: + ... + + Lock objects can be tested for locking state: + + if not lock.locked(): + await lock.acquire() + else: + # lock is acquired + ... + + """ + + def __init__(self) -> None: + self._waiters: Optional[collections.deque] = None + self._locked = False + + def __repr__(self) -> str: + res = super().__repr__() + extra = "locked" if self._locked else "unlocked" + if self._waiters: + extra = f"{extra}, waiters:{len(self._waiters)}" + return f"<{res[1:-1]} [{extra}]>" + + def locked(self) -> bool: + """Return True if lock is acquired.""" + return self._locked + + async def acquire(self) -> bool: + """Acquire a lock. + + This method blocks until the lock is unlocked, then sets it to + locked and returns True. + """ + # Implement fair scheduling, where thread always waits + # its turn. Jumping the queue if all are cancelled is an optimization. + if not self._locked and ( + self._waiters is None or all(w.cancelled() for w in self._waiters) + ): + self._locked = True + return True + + if self._waiters is None: + self._waiters = collections.deque() + fut = self._get_loop().create_future() + self._waiters.append(fut) + + try: + try: + await fut + finally: + self._waiters.remove(fut) + except exceptions.CancelledError: + # Currently the only exception designed be able to occur here. + + # Ensure the lock invariant: If lock is not claimed (or about + # to be claimed by us) and there is a Task in waiters, + # ensure that the Task at the head will run. + if not self._locked: + self._wake_up_first() + raise + + # assert self._locked is False + self._locked = True + return True + + def release(self) -> None: + """Release a lock. + + When the lock is locked, reset it to unlocked, and return. + If any other tasks are blocked waiting for the lock to become + unlocked, allow exactly one of them to proceed. + + When invoked on an unlocked lock, a RuntimeError is raised. + + There is no return value. + """ + if self._locked: + self._locked = False + self._wake_up_first() + else: + raise RuntimeError("Lock is not acquired.") + + def _wake_up_first(self) -> None: + """Ensure that the first waiter will wake up.""" + if not self._waiters: + return + try: + fut = next(iter(self._waiters)) + except StopIteration: + return + + # .done() means that the waiter is already set to wake up. + if not fut.done(): + fut.set_result(True) + + +class Condition(_ContextManagerMixin, _LoopBoundMixin): + """Asynchronous equivalent to threading.Condition. + + This class implements condition variable objects. A condition variable + allows one or more tasks to wait until they are notified by another + task. + + A new Lock object is created and used as the underlying lock. + """ + + def __init__(self, lock: Optional[Lock] = None) -> None: + if lock is None: + lock = Lock() + + self._lock = lock + # Export the lock's locked(), acquire() and release() methods. + self.locked = lock.locked + self.acquire = lock.acquire + self.release = lock.release + + self._waiters: collections.deque = collections.deque() + + def __repr__(self) -> str: + res = super().__repr__() + extra = "locked" if self.locked() else "unlocked" + if self._waiters: + extra = f"{extra}, waiters:{len(self._waiters)}" + return f"<{res[1:-1]} [{extra}]>" + + async def wait(self) -> bool: + """Wait until notified. + + If the calling task has not acquired the lock when this + method is called, a RuntimeError is raised. + + This method releases the underlying lock, and then blocks + until it is awakened by a notify() or notify_all() call for + the same condition variable in another task. Once + awakened, it re-acquires the lock and returns True. + + This method may return spuriously, + which is why the caller should always + re-check the state and be prepared to wait() again. + """ + if not self.locked(): + raise RuntimeError("cannot wait on un-acquired lock") + + fut = self._get_loop().create_future() + self.release() + try: + try: + self._waiters.append(fut) + try: + await fut + return True + finally: + self._waiters.remove(fut) + + finally: + # Must re-acquire lock even if wait is cancelled. + # We only catch CancelledError here, since we don't want any + # other (fatal) errors with the future to cause us to spin. + err = None + while True: + try: + await self.acquire() + break + except exceptions.CancelledError as e: + err = e + + if err is not None: + try: + raise err # Re-raise most recent exception instance. + finally: + err = None # Break reference cycles. + except BaseException: + # Any error raised out of here _may_ have occurred after this Task + # believed to have been successfully notified. + # Make sure to notify another Task instead. This may result + # in a "spurious wakeup", which is allowed as part of the + # Condition Variable protocol. + self._notify(1) + raise + + async def wait_for(self, predicate: Any) -> Coroutine: + """Wait until a predicate becomes true. + + The predicate should be a callable whose result will be + interpreted as a boolean value. The method will repeatedly + wait() until it evaluates to true. The final predicate value is + the return value. + """ + result = predicate() + while not result: + await self.wait() + result = predicate() + return result + + def notify(self, n: int = 1) -> None: + """By default, wake up one task waiting on this condition, if any. + If the calling task has not acquired the lock when this method + is called, a RuntimeError is raised. + + This method wakes up n of the tasks waiting for the condition + variable; if fewer than n are waiting, they are all awoken. + + Note: an awakened task does not actually return from its + wait() call until it can reacquire the lock. Since notify() does + not release the lock, its caller should. + """ + if not self.locked(): + raise RuntimeError("cannot notify on un-acquired lock") + self._notify(n) + + def _notify(self, n: int) -> None: + idx = 0 + for fut in self._waiters: + if idx >= n: + break + + if not fut.done(): + idx += 1 + fut.set_result(False) + + def notify_all(self) -> None: + """Wake up all tasks waiting on this condition. This method acts + like notify(), but wakes up all waiting tasks instead of one. If the + calling task has not acquired the lock when this method is called, + a RuntimeError is raised. + """ + self.notify(len(self._waiters)) diff --git a/pymongo/asynchronous/cursor.py b/pymongo/asynchronous/cursor.py index 4b4bb52a8e..7d7ae4a5db 100644 --- a/pymongo/asynchronous/cursor.py +++ b/pymongo/asynchronous/cursor.py @@ -45,7 +45,7 @@ ) from pymongo.cursor_shared import _CURSOR_CLOSED_ERRORS, _QUERY_OPTIONS, CursorType, _Hint, _Sort from pymongo.errors import ConnectionFailure, InvalidOperation, OperationFailure -from pymongo.lock import _ALock, _create_lock +from pymongo.lock import _async_create_lock from pymongo.message import ( _CursorAddress, _GetMore, @@ -77,7 +77,7 @@ class _ConnectionManager: def __init__(self, conn: AsyncConnection, more_to_come: bool): self.conn: Optional[AsyncConnection] = conn self.more_to_come = more_to_come - self._alock = _ALock(_create_lock()) + self._lock = _async_create_lock() def update_exhaust(self, more_to_come: bool) -> None: self.more_to_come = more_to_come diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index eae2b0df4c..a33246a24b 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -82,7 +82,11 @@ WaitQueueTimeoutError, WriteConcernError, ) -from pymongo.lock import _HAS_REGISTER_AT_FORK, _ALock, _create_lock, _release_locks +from pymongo.lock import ( + _HAS_REGISTER_AT_FORK, + _async_create_lock, + _release_locks, +) from pymongo.logger import _CLIENT_LOGGER, _log_or_warn from pymongo.message import _CursorAddress, _GetMore, _Query from pymongo.monitoring import ConnectionClosedReason @@ -842,7 +846,7 @@ def __init__( self._options = options = ClientOptions(username, password, dbase, opts, _IS_SYNC) self._default_database_name = dbase - self._lock = _ALock(_create_lock()) + self._lock = _async_create_lock() self._kill_cursors_queue: list = [] self._event_listeners = options.pool_options._event_listeners @@ -1721,7 +1725,7 @@ async def _run_operation( address=address, ) - async with operation.conn_mgr._alock: + async with operation.conn_mgr._lock: async with _MongoClientErrorHandler(self, server, operation.session) as err_handler: # type: ignore[arg-type] err_handler.contribute_socket(operation.conn_mgr.conn) return await server.run_operation( @@ -1969,7 +1973,7 @@ async def _close_cursor_now( try: if conn_mgr: - async with conn_mgr._alock: + async with conn_mgr._lock: # Cursor is pinned to LB outside of a transaction. assert address is not None assert conn_mgr.conn is not None diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index a9f02d650a..2fe9579aef 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -23,7 +23,6 @@ import socket import ssl import sys -import threading import time import weakref from typing import ( @@ -65,7 +64,11 @@ _CertificateError, ) from pymongo.hello import Hello, HelloCompat -from pymongo.lock import _ACondition, _ALock, _create_lock +from pymongo.lock import ( + _async_cond_wait, + _async_create_condition, + _async_create_lock, +) from pymongo.logger import ( _CONNECTION_LOGGER, _ConnectionStatusMessage, @@ -208,11 +211,6 @@ def _raise_connection_failure( raise AutoReconnect(msg) from error -async def _cond_wait(condition: _ACondition, deadline: Optional[float]) -> bool: - timeout = deadline - time.monotonic() if deadline else None - return await condition.wait(timeout) - - def _get_timeout_details(options: PoolOptions) -> dict[str, float]: details = {} timeout = _csot.get_timeout() @@ -992,8 +990,8 @@ def __init__( # from the right side. self.conns: collections.deque = collections.deque() self.active_contexts: set[_CancellationContext] = set() - _lock = _create_lock() - self.lock = _ALock(_lock) + self.lock = _async_create_lock() + self._max_connecting_cond = _async_create_condition(self.lock) self.active_sockets = 0 # Monotonically increasing connection ID required for CMAP Events. self.next_connection_id = 1 @@ -1019,7 +1017,7 @@ def __init__( # The first portion of the wait queue. # Enforces: maxPoolSize # Also used for: clearing the wait queue - self.size_cond = _ACondition(threading.Condition(_lock)) + self.size_cond = _async_create_condition(self.lock) self.requests = 0 self.max_pool_size = self.opts.max_pool_size if not self.max_pool_size: @@ -1027,7 +1025,7 @@ def __init__( # The second portion of the wait queue. # Enforces: maxConnecting # Also used for: clearing the wait queue - self._max_connecting_cond = _ACondition(threading.Condition(_lock)) + self._max_connecting_cond = _async_create_condition(self.lock) self._max_connecting = self.opts.max_connecting self._pending = 0 self._client_id = client_id @@ -1456,7 +1454,8 @@ async def _get_conn( async with self.size_cond: self._raise_if_not_ready(checkout_started_time, emit_event=True) while not (self.requests < self.max_pool_size): - if not await _cond_wait(self.size_cond, deadline): + timeout = deadline - time.monotonic() if deadline else None + if not await _async_cond_wait(self.size_cond, timeout): # Timed out, notify the next thread to ensure a # timeout doesn't consume the condition. if self.requests < self.max_pool_size: @@ -1479,7 +1478,8 @@ async def _get_conn( async with self._max_connecting_cond: self._raise_if_not_ready(checkout_started_time, emit_event=False) while not (self.conns or self._pending < self._max_connecting): - if not await _cond_wait(self._max_connecting_cond, deadline): + timeout = deadline - time.monotonic() if deadline else None + if not await _async_cond_wait(self._max_connecting_cond, timeout): # Timed out, notify the next thread to ensure a # timeout doesn't consume the condition. if self.conns or self._pending < self._max_connecting: diff --git a/pymongo/asynchronous/topology.py b/pymongo/asynchronous/topology.py index f0cb56cbf1..6d67710a7e 100644 --- a/pymongo/asynchronous/topology.py +++ b/pymongo/asynchronous/topology.py @@ -43,7 +43,11 @@ WriteError, ) from pymongo.hello import Hello -from pymongo.lock import _ACondition, _ALock, _create_lock +from pymongo.lock import ( + _async_cond_wait, + _async_create_condition, + _async_create_lock, +) from pymongo.logger import ( _SDAM_LOGGER, _SERVER_SELECTION_LOGGER, @@ -169,9 +173,10 @@ def __init__(self, topology_settings: TopologySettings): self._seed_addresses = list(topology_description.server_descriptions()) self._opened = False self._closed = False - _lock = _create_lock() - self._lock = _ALock(_lock) - self._condition = _ACondition(self._settings.condition_class(_lock)) + self._lock = _async_create_lock() + self._condition = _async_create_condition( + self._lock, self._settings.condition_class if _IS_SYNC else None + ) self._servers: dict[_Address, Server] = {} self._pid: Optional[int] = None self._max_cluster_time: Optional[ClusterTime] = None @@ -353,7 +358,7 @@ async def _select_servers_loop( # change, or for a timeout. We won't miss any changes that # came after our most recent apply_selector call, since we've # held the lock until now. - await self._condition.wait(common.MIN_HEARTBEAT_INTERVAL) + await _async_cond_wait(self._condition, common.MIN_HEARTBEAT_INTERVAL) self._description.check_compatible() now = time.monotonic() server_descriptions = self._description.apply_selector( @@ -653,7 +658,7 @@ async def request_check_all(self, wait_time: int = 5) -> None: """Wake all monitors, wait for at least one to check its server.""" async with self._lock: self._request_check_all() - await self._condition.wait(wait_time) + await _async_cond_wait(self._condition, wait_time) def data_bearing_servers(self) -> list[ServerDescription]: """Return a list of all data-bearing servers. diff --git a/pymongo/lock.py b/pymongo/lock.py index 0cbfb4a57e..6bf7138017 100644 --- a/pymongo/lock.py +++ b/pymongo/lock.py @@ -11,15 +11,20 @@ # 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. + +"""Internal helpers for lock and condition coordination primitives.""" + from __future__ import annotations import asyncio -import collections import os +import sys import threading -import time import weakref -from typing import Any, Callable, Optional, TypeVar +from asyncio import wait_for +from typing import Any, Optional, TypeVar + +import pymongo._asyncio_lock _HAS_REGISTER_AT_FORK = hasattr(os, "register_at_fork") @@ -28,6 +33,15 @@ _T = TypeVar("_T") +# Needed to support 3.13 asyncio fixes (https://github.com/python/cpython/issues/112202) +# in older versions of Python +if sys.version_info >= (3, 13): + Lock = asyncio.Lock + Condition = asyncio.Condition +else: + Lock = pymongo._asyncio_lock.Lock + Condition = pymongo._asyncio_lock.Condition + def _create_lock() -> threading.Lock: """Represents a lock that is tracked upon instantiation using a WeakSet and @@ -39,6 +53,27 @@ def _create_lock() -> threading.Lock: return lock +def _async_create_lock() -> Lock: + """Represents an asyncio.Lock.""" + return Lock() + + +def _create_condition( + lock: threading.Lock, condition_class: Optional[Any] = None +) -> threading.Condition: + """Represents a threading.Condition.""" + if condition_class: + return condition_class(lock) + return threading.Condition(lock) + + +def _async_create_condition(lock: Lock, condition_class: Optional[Any] = None) -> Condition: + """Represents an asyncio.Condition.""" + if condition_class: + return condition_class(lock) + return Condition(lock) + + def _release_locks() -> None: # Completed the fork, reset all the locks in the child. for lock in _forkable_locks: @@ -46,202 +81,12 @@ def _release_locks() -> None: lock.release() -# Needed only for synchro.py compat. -def _Lock(lock: threading.Lock) -> threading.Lock: - return lock +async def _async_cond_wait(condition: Condition, timeout: Optional[float]) -> bool: + try: + return await wait_for(condition.wait(), timeout) + except asyncio.TimeoutError: + return False -class _ALock: - __slots__ = ("_lock",) - - def __init__(self, lock: threading.Lock) -> None: - self._lock = lock - - def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: - return self._lock.acquire(blocking=blocking, timeout=timeout) - - async def a_acquire(self, blocking: bool = True, timeout: float = -1) -> bool: - if timeout > 0: - tstart = time.monotonic() - while True: - acquired = self._lock.acquire(blocking=False) - if acquired: - return True - if timeout > 0 and (time.monotonic() - tstart) > timeout: - return False - if not blocking: - return False - await asyncio.sleep(0) - - def release(self) -> None: - self._lock.release() - - async def __aenter__(self) -> _ALock: - await self.a_acquire() - return self - - def __enter__(self) -> _ALock: - self._lock.acquire() - return self - - def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: - self.release() - - async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: - self.release() - - -def _safe_set_result(fut: asyncio.Future) -> None: - # Ensure the future hasn't been cancelled before calling set_result. - if not fut.done(): - fut.set_result(False) - - -class _ACondition: - __slots__ = ("_condition", "_waiters") - - def __init__(self, condition: threading.Condition) -> None: - self._condition = condition - self._waiters: collections.deque = collections.deque() - - async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: - if timeout > 0: - tstart = time.monotonic() - while True: - acquired = self._condition.acquire(blocking=False) - if acquired: - return True - if timeout > 0 and (time.monotonic() - tstart) > timeout: - return False - if not blocking: - return False - await asyncio.sleep(0) - - async def wait(self, timeout: Optional[float] = None) -> bool: - """Wait until notified. - - If the calling task has not acquired the lock when this - method is called, a RuntimeError is raised. - - This method releases the underlying lock, and then blocks - until it is awakened by a notify() or notify_all() call for - the same condition variable in another task. Once - awakened, it re-acquires the lock and returns True. - - This method may return spuriously, - which is why the caller should always - re-check the state and be prepared to wait() again. - """ - loop = asyncio.get_running_loop() - fut = loop.create_future() - self._waiters.append((loop, fut)) - self.release() - try: - try: - try: - await asyncio.wait_for(fut, timeout) - return True - except asyncio.TimeoutError: - return False # Return false on timeout for sync pool compat. - finally: - # Must re-acquire lock even if wait is cancelled. - # We only catch CancelledError here, since we don't want any - # other (fatal) errors with the future to cause us to spin. - err = None - while True: - try: - await self.acquire() - break - except asyncio.exceptions.CancelledError as e: - err = e - - self._waiters.remove((loop, fut)) - if err is not None: - try: - raise err # Re-raise most recent exception instance. - finally: - err = None # Break reference cycles. - except BaseException: - # Any error raised out of here _may_ have occurred after this Task - # believed to have been successfully notified. - # Make sure to notify another Task instead. This may result - # in a "spurious wakeup", which is allowed as part of the - # Condition Variable protocol. - self.notify(1) - raise - - async def wait_for(self, predicate: Callable[[], _T]) -> _T: - """Wait until a predicate becomes true. - - The predicate should be a callable whose result will be - interpreted as a boolean value. The method will repeatedly - wait() until it evaluates to true. The final predicate value is - the return value. - """ - result = predicate() - while not result: - await self.wait() - result = predicate() - return result - - def notify(self, n: int = 1) -> None: - """By default, wake up one coroutine waiting on this condition, if any. - If the calling coroutine has not acquired the lock when this method - is called, a RuntimeError is raised. - - This method wakes up at most n of the coroutines waiting for the - condition variable; it is a no-op if no coroutines are waiting. - - Note: an awakened coroutine does not actually return from its - wait() call until it can reacquire the lock. Since notify() does - not release the lock, its caller should. - """ - idx = 0 - to_remove = [] - for loop, fut in self._waiters: - if idx >= n: - break - - if fut.done(): - continue - - try: - loop.call_soon_threadsafe(_safe_set_result, fut) - except RuntimeError: - # Loop was closed, ignore. - to_remove.append((loop, fut)) - continue - - idx += 1 - - for waiter in to_remove: - self._waiters.remove(waiter) - - def notify_all(self) -> None: - """Wake up all threads waiting on this condition. This method acts - like notify(), but wakes up all waiting threads instead of one. If the - calling thread has not acquired the lock when this method is called, - a RuntimeError is raised. - """ - self.notify(len(self._waiters)) - - def locked(self) -> bool: - """Only needed for tests in test_locks.""" - return self._condition._lock.locked() # type: ignore[attr-defined] - - def release(self) -> None: - self._condition.release() - - async def __aenter__(self) -> _ACondition: - await self.acquire() - return self - - def __enter__(self) -> _ACondition: - self._condition.acquire() - return self - - async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: - self.release() - - def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None: - self.release() +def _cond_wait(condition: threading.Condition, timeout: Optional[float]) -> bool: + return condition.wait(timeout) diff --git a/pymongo/synchronous/cursor.py b/pymongo/synchronous/cursor.py index 27a76cf91d..9a7637704f 100644 --- a/pymongo/synchronous/cursor.py +++ b/pymongo/synchronous/cursor.py @@ -77,7 +77,7 @@ class _ConnectionManager: def __init__(self, conn: Connection, more_to_come: bool): self.conn: Optional[Connection] = conn self.more_to_come = more_to_come - self._alock = _create_lock() + self._lock = _create_lock() def update_exhaust(self, more_to_come: bool) -> None: self.more_to_come = more_to_come diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 7eab5e74f1..eb363f82f5 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -74,7 +74,11 @@ WaitQueueTimeoutError, WriteConcernError, ) -from pymongo.lock import _HAS_REGISTER_AT_FORK, _create_lock, _release_locks +from pymongo.lock import ( + _HAS_REGISTER_AT_FORK, + _create_lock, + _release_locks, +) from pymongo.logger import _CLIENT_LOGGER, _log_or_warn from pymongo.message import _CursorAddress, _GetMore, _Query from pymongo.monitoring import ConnectionClosedReason @@ -1715,7 +1719,7 @@ def _run_operation( address=address, ) - with operation.conn_mgr._alock: + with operation.conn_mgr._lock: with _MongoClientErrorHandler(self, server, operation.session) as err_handler: # type: ignore[arg-type] err_handler.contribute_socket(operation.conn_mgr.conn) return server.run_operation( @@ -1963,7 +1967,7 @@ def _close_cursor_now( try: if conn_mgr: - with conn_mgr._alock: + with conn_mgr._lock: # Cursor is pinned to LB outside of a transaction. assert address is not None assert conn_mgr.conn is not None diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index eb007a3471..6ac7b4eca9 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -23,7 +23,6 @@ import socket import ssl import sys -import threading import time import weakref from typing import ( @@ -62,7 +61,11 @@ _CertificateError, ) from pymongo.hello import Hello, HelloCompat -from pymongo.lock import _create_lock, _Lock +from pymongo.lock import ( + _cond_wait, + _create_condition, + _create_lock, +) from pymongo.logger import ( _CONNECTION_LOGGER, _ConnectionStatusMessage, @@ -208,11 +211,6 @@ def _raise_connection_failure( raise AutoReconnect(msg) from error -def _cond_wait(condition: threading.Condition, deadline: Optional[float]) -> bool: - timeout = deadline - time.monotonic() if deadline else None - return condition.wait(timeout) - - def _get_timeout_details(options: PoolOptions) -> dict[str, float]: details = {} timeout = _csot.get_timeout() @@ -988,8 +986,8 @@ def __init__( # from the right side. self.conns: collections.deque = collections.deque() self.active_contexts: set[_CancellationContext] = set() - _lock = _create_lock() - self.lock = _Lock(_lock) + self.lock = _create_lock() + self._max_connecting_cond = _create_condition(self.lock) self.active_sockets = 0 # Monotonically increasing connection ID required for CMAP Events. self.next_connection_id = 1 @@ -1015,7 +1013,7 @@ def __init__( # The first portion of the wait queue. # Enforces: maxPoolSize # Also used for: clearing the wait queue - self.size_cond = threading.Condition(_lock) + self.size_cond = _create_condition(self.lock) self.requests = 0 self.max_pool_size = self.opts.max_pool_size if not self.max_pool_size: @@ -1023,7 +1021,7 @@ def __init__( # The second portion of the wait queue. # Enforces: maxConnecting # Also used for: clearing the wait queue - self._max_connecting_cond = threading.Condition(_lock) + self._max_connecting_cond = _create_condition(self.lock) self._max_connecting = self.opts.max_connecting self._pending = 0 self._client_id = client_id @@ -1450,7 +1448,8 @@ def _get_conn( with self.size_cond: self._raise_if_not_ready(checkout_started_time, emit_event=True) while not (self.requests < self.max_pool_size): - if not _cond_wait(self.size_cond, deadline): + timeout = deadline - time.monotonic() if deadline else None + if not _cond_wait(self.size_cond, timeout): # Timed out, notify the next thread to ensure a # timeout doesn't consume the condition. if self.requests < self.max_pool_size: @@ -1473,7 +1472,8 @@ def _get_conn( with self._max_connecting_cond: self._raise_if_not_ready(checkout_started_time, emit_event=False) while not (self.conns or self._pending < self._max_connecting): - if not _cond_wait(self._max_connecting_cond, deadline): + timeout = deadline - time.monotonic() if deadline else None + if not _cond_wait(self._max_connecting_cond, timeout): # Timed out, notify the next thread to ensure a # timeout doesn't consume the condition. if self.conns or self._pending < self._max_connecting: diff --git a/pymongo/synchronous/topology.py b/pymongo/synchronous/topology.py index e34de6bc50..b03269ae43 100644 --- a/pymongo/synchronous/topology.py +++ b/pymongo/synchronous/topology.py @@ -39,7 +39,11 @@ WriteError, ) from pymongo.hello import Hello -from pymongo.lock import _create_lock, _Lock +from pymongo.lock import ( + _cond_wait, + _create_condition, + _create_lock, +) from pymongo.logger import ( _SDAM_LOGGER, _SERVER_SELECTION_LOGGER, @@ -169,9 +173,10 @@ def __init__(self, topology_settings: TopologySettings): self._seed_addresses = list(topology_description.server_descriptions()) self._opened = False self._closed = False - _lock = _create_lock() - self._lock = _Lock(_lock) - self._condition = self._settings.condition_class(_lock) + self._lock = _create_lock() + self._condition = _create_condition( + self._lock, self._settings.condition_class if _IS_SYNC else None + ) self._servers: dict[_Address, Server] = {} self._pid: Optional[int] = None self._max_cluster_time: Optional[ClusterTime] = None @@ -353,7 +358,7 @@ def _select_servers_loop( # change, or for a timeout. We won't miss any changes that # came after our most recent apply_selector call, since we've # held the lock until now. - self._condition.wait(common.MIN_HEARTBEAT_INTERVAL) + _cond_wait(self._condition, common.MIN_HEARTBEAT_INTERVAL) self._description.check_compatible() now = time.monotonic() server_descriptions = self._description.apply_selector( @@ -651,7 +656,7 @@ def request_check_all(self, wait_time: int = 5) -> None: """Wake all monitors, wait for at least one to check its server.""" with self._lock: self._request_check_all() - self._condition.wait(wait_time) + _cond_wait(self._condition, wait_time) def data_bearing_servers(self) -> list[ServerDescription]: """Return a list of all data-bearing servers. diff --git a/test/__init__.py b/test/__init__.py index 940518c2c5..c1944f5870 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1131,7 +1131,7 @@ class IntegrationTest(PyMongoTestCase): @client_context.require_connection def setUp(self) -> None: - if not _IS_SYNC: + if not _IS_SYNC and client_context.client is not None: reset_client_context() if client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 8d1e3e1911..9ca5a32ffc 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -1149,7 +1149,7 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase): @async_client_context.require_connection async def asyncSetUp(self) -> None: - if not _IS_SYNC: + if not _IS_SYNC and async_client_context.client is not None: await reset_client_context() if async_client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") diff --git a/test/asynchronous/test_locks.py b/test/asynchronous/test_locks.py index e0e7f2fc8d..e5a0adfee6 100644 --- a/test/asynchronous/test_locks.py +++ b/test/asynchronous/test_locks.py @@ -16,498 +16,447 @@ import asyncio import sys -import threading import unittest +from pymongo.lock import _async_create_condition, _async_create_lock + sys.path[0:0] = [""] -from pymongo.lock import _ACondition +if sys.version_info < (3, 13): + # Tests adapted from: https://github.com/python/cpython/blob/v3.13.0rc2/Lib/test/test_asyncio/test_locks.py + # Includes tests for: + # - https://github.com/python/cpython/issues/111693 + # - https://github.com/python/cpython/issues/112202 + class TestConditionStdlib(unittest.IsolatedAsyncioTestCase): + async def test_wait(self): + cond = _async_create_condition(_async_create_lock()) + result = [] + + async def c1(result): + await cond.acquire() + if await cond.wait(): + result.append(1) + return True + async def c2(result): + await cond.acquire() + if await cond.wait(): + result.append(2) + return True -# Tests adapted from: https://github.com/python/cpython/blob/v3.13.0rc2/Lib/test/test_asyncio/test_locks.py -# Includes tests for: -# - https://github.com/python/cpython/issues/111693 -# - https://github.com/python/cpython/issues/112202 -class TestConditionStdlib(unittest.IsolatedAsyncioTestCase): - async def test_wait(self): - cond = _ACondition(threading.Condition(threading.Lock())) - result = [] + async def c3(result): + await cond.acquire() + if await cond.wait(): + result.append(3) + return True - async def c1(result): - await cond.acquire() - if await cond.wait(): - result.append(1) - return True + t1 = asyncio.create_task(c1(result)) + t2 = asyncio.create_task(c2(result)) + t3 = asyncio.create_task(c3(result)) - async def c2(result): - await cond.acquire() - if await cond.wait(): - result.append(2) - return True + await asyncio.sleep(0) + self.assertEqual([], result) + self.assertFalse(cond.locked()) - async def c3(result): - await cond.acquire() - if await cond.wait(): - result.append(3) - return True - - t1 = asyncio.create_task(c1(result)) - t2 = asyncio.create_task(c2(result)) - t3 = asyncio.create_task(c3(result)) - - await asyncio.sleep(0) - self.assertEqual([], result) - self.assertFalse(cond.locked()) - - self.assertTrue(await cond.acquire()) - cond.notify() - await asyncio.sleep(0) - self.assertEqual([], result) - self.assertTrue(cond.locked()) - - cond.release() - await asyncio.sleep(0) - self.assertEqual([1], result) - self.assertTrue(cond.locked()) - - cond.notify(2) - await asyncio.sleep(0) - self.assertEqual([1], result) - self.assertTrue(cond.locked()) - - cond.release() - await asyncio.sleep(0) - self.assertEqual([1, 2], result) - self.assertTrue(cond.locked()) - - cond.release() - await asyncio.sleep(0) - self.assertEqual([1, 2, 3], result) - self.assertTrue(cond.locked()) - - self.assertTrue(t1.done()) - self.assertTrue(t1.result()) - self.assertTrue(t2.done()) - self.assertTrue(t2.result()) - self.assertTrue(t3.done()) - self.assertTrue(t3.result()) - - async def test_wait_cancel(self): - cond = _ACondition(threading.Condition(threading.Lock())) - await cond.acquire() - - wait = asyncio.create_task(cond.wait()) - asyncio.get_running_loop().call_soon(wait.cancel) - with self.assertRaises(asyncio.CancelledError): - await wait - self.assertFalse(cond._waiters) - self.assertTrue(cond.locked()) - - async def test_wait_cancel_contested(self): - cond = _ACondition(threading.Condition(threading.Lock())) - - await cond.acquire() - self.assertTrue(cond.locked()) - - wait_task = asyncio.create_task(cond.wait()) - await asyncio.sleep(0) - self.assertFalse(cond.locked()) - - # Notify, but contest the lock before cancelling - await cond.acquire() - self.assertTrue(cond.locked()) - cond.notify() - asyncio.get_running_loop().call_soon(wait_task.cancel) - asyncio.get_running_loop().call_soon(cond.release) - - try: - await wait_task - except asyncio.CancelledError: - # Should not happen, since no cancellation points - pass - - self.assertTrue(cond.locked()) - - async def test_wait_cancel_after_notify(self): - # See bpo-32841 - waited = False - - cond = _ACondition(threading.Condition(threading.Lock())) - - async def wait_on_cond(): - nonlocal waited - async with cond: - waited = True # Make sure this area was reached - await cond.wait() + self.assertTrue(await cond.acquire()) + cond.notify() + await asyncio.sleep(0) + self.assertEqual([], result) + self.assertTrue(cond.locked()) - waiter = asyncio.create_task(wait_on_cond()) - await asyncio.sleep(0) # Start waiting + cond.release() + await asyncio.sleep(0) + self.assertEqual([1], result) + self.assertTrue(cond.locked()) + + cond.notify(2) + await asyncio.sleep(0) + self.assertEqual([1], result) + self.assertTrue(cond.locked()) - await cond.acquire() - cond.notify() - await asyncio.sleep(0) # Get to acquire() - waiter.cancel() - await asyncio.sleep(0) # Activate cancellation - cond.release() - await asyncio.sleep(0) # Cancellation should occur + cond.release() + await asyncio.sleep(0) + self.assertEqual([1, 2], result) + self.assertTrue(cond.locked()) - self.assertTrue(waiter.cancelled()) - self.assertTrue(waited) + cond.release() + await asyncio.sleep(0) + self.assertEqual([1, 2, 3], result) + self.assertTrue(cond.locked()) - async def test_wait_unacquired(self): - cond = _ACondition(threading.Condition(threading.Lock())) - with self.assertRaises(RuntimeError): - await cond.wait() + self.assertTrue(t1.done()) + self.assertTrue(t1.result()) + self.assertTrue(t2.done()) + self.assertTrue(t2.result()) + self.assertTrue(t3.done()) + self.assertTrue(t3.result()) - async def test_wait_for(self): - cond = _ACondition(threading.Condition(threading.Lock())) - presult = False + async def test_wait_cancel(self): + cond = _async_create_condition(_async_create_lock()) + await cond.acquire() - def predicate(): - return presult + wait = asyncio.create_task(cond.wait()) + asyncio.get_running_loop().call_soon(wait.cancel) + with self.assertRaises(asyncio.CancelledError): + await wait + self.assertFalse(cond._waiters) + self.assertTrue(cond.locked()) - result = [] + async def test_wait_cancel_contested(self): + cond = _async_create_condition(_async_create_lock()) - async def c1(result): await cond.acquire() - if await cond.wait_for(predicate): - result.append(1) - cond.release() - return True + self.assertTrue(cond.locked()) - t = asyncio.create_task(c1(result)) + wait_task = asyncio.create_task(cond.wait()) + await asyncio.sleep(0) + self.assertFalse(cond.locked()) - await asyncio.sleep(0) - self.assertEqual([], result) + # Notify, but contest the lock before cancelling + await cond.acquire() + self.assertTrue(cond.locked()) + cond.notify() + asyncio.get_running_loop().call_soon(wait_task.cancel) + asyncio.get_running_loop().call_soon(cond.release) - await cond.acquire() - cond.notify() - cond.release() - await asyncio.sleep(0) - self.assertEqual([], result) + try: + await wait_task + except asyncio.CancelledError: + # Should not happen, since no cancellation points + pass - presult = True - await cond.acquire() - cond.notify() - cond.release() - await asyncio.sleep(0) - self.assertEqual([1], result) + self.assertTrue(cond.locked()) - self.assertTrue(t.done()) - self.assertTrue(t.result()) + async def test_wait_cancel_after_notify(self): + # See bpo-32841 + waited = False - async def test_wait_for_unacquired(self): - cond = _ACondition(threading.Condition(threading.Lock())) + cond = _async_create_condition(_async_create_lock()) - # predicate can return true immediately - res = await cond.wait_for(lambda: [1, 2, 3]) - self.assertEqual([1, 2, 3], res) + async def wait_on_cond(): + nonlocal waited + async with cond: + waited = True # Make sure this area was reached + await cond.wait() - with self.assertRaises(RuntimeError): - await cond.wait_for(lambda: False) + waiter = asyncio.create_task(wait_on_cond()) + await asyncio.sleep(0) # Start waiting - async def test_notify(self): - cond = _ACondition(threading.Condition(threading.Lock())) - result = [] + await cond.acquire() + cond.notify() + await asyncio.sleep(0) # Get to acquire() + waiter.cancel() + await asyncio.sleep(0) # Activate cancellation + cond.release() + await asyncio.sleep(0) # Cancellation should occur + + self.assertTrue(waiter.cancelled()) + self.assertTrue(waited) + + async def test_wait_unacquired(self): + cond = _async_create_condition(_async_create_lock()) + with self.assertRaises(RuntimeError): + await cond.wait() - async def c1(result): - async with cond: - if await cond.wait(): - result.append(1) - return True + async def test_wait_for(self): + cond = _async_create_condition(_async_create_lock()) + presult = False - async def c2(result): - async with cond: - if await cond.wait(): - result.append(2) - return True + def predicate(): + return presult - async def c3(result): - async with cond: - if await cond.wait(): - result.append(3) + result = [] + + async def c1(result): + await cond.acquire() + if await cond.wait_for(predicate): + result.append(1) + cond.release() return True - t1 = asyncio.create_task(c1(result)) - t2 = asyncio.create_task(c2(result)) - t3 = asyncio.create_task(c3(result)) + t = asyncio.create_task(c1(result)) - await asyncio.sleep(0) - self.assertEqual([], result) + await asyncio.sleep(0) + self.assertEqual([], result) - async with cond: - cond.notify(1) - await asyncio.sleep(1) - self.assertEqual([1], result) + await cond.acquire() + cond.notify() + cond.release() + await asyncio.sleep(0) + self.assertEqual([], result) - async with cond: - cond.notify(1) - cond.notify(2048) - await asyncio.sleep(1) - self.assertEqual([1, 2, 3], result) + presult = True + await cond.acquire() + cond.notify() + cond.release() + await asyncio.sleep(0) + self.assertEqual([1], result) - self.assertTrue(t1.done()) - self.assertTrue(t1.result()) - self.assertTrue(t2.done()) - self.assertTrue(t2.result()) - self.assertTrue(t3.done()) - self.assertTrue(t3.result()) + self.assertTrue(t.done()) + self.assertTrue(t.result()) - async def test_notify_all(self): - cond = _ACondition(threading.Condition(threading.Lock())) + async def test_wait_for_unacquired(self): + cond = _async_create_condition(_async_create_lock()) - result = [] + # predicate can return true immediately + res = await cond.wait_for(lambda: [1, 2, 3]) + self.assertEqual([1, 2, 3], res) - async def c1(result): - async with cond: - if await cond.wait(): - result.append(1) - return True + with self.assertRaises(RuntimeError): + await cond.wait_for(lambda: False) - async def c2(result): - async with cond: - if await cond.wait(): - result.append(2) - return True + async def test_notify(self): + cond = _async_create_condition(_async_create_lock()) + result = [] - t1 = asyncio.create_task(c1(result)) - t2 = asyncio.create_task(c2(result)) + async def c1(result): + async with cond: + if await cond.wait(): + result.append(1) + return True - await asyncio.sleep(0) - self.assertEqual([], result) + async def c2(result): + async with cond: + if await cond.wait(): + result.append(2) + return True - async with cond: - cond.notify_all() - await asyncio.sleep(1) - self.assertEqual([1, 2], result) + async def c3(result): + async with cond: + if await cond.wait(): + result.append(3) + return True - self.assertTrue(t1.done()) - self.assertTrue(t1.result()) - self.assertTrue(t2.done()) - self.assertTrue(t2.result()) + t1 = asyncio.create_task(c1(result)) + t2 = asyncio.create_task(c2(result)) + t3 = asyncio.create_task(c3(result)) - async def test_context_manager(self): - cond = _ACondition(threading.Condition(threading.Lock())) - self.assertFalse(cond.locked()) - async with cond: - self.assertTrue(cond.locked()) - self.assertFalse(cond.locked()) - - async def test_timeout_in_block(self): - condition = _ACondition(threading.Condition(threading.Lock())) - async with condition: - with self.assertRaises(asyncio.TimeoutError): - await asyncio.wait_for(condition.wait(), timeout=0.5) - - @unittest.skipIf( - sys.version_info < (3, 11), "raising the same cancelled error requires Python>=3.11" - ) - async def test_cancelled_error_wakeup(self): - # Test that a cancelled error, received when awaiting wakeup, - # will be re-raised un-modified. - wake = False - raised = None - cond = _ACondition(threading.Condition(threading.Lock())) - - async def func(): - nonlocal raised - async with cond: - with self.assertRaises(asyncio.CancelledError) as err: - await cond.wait_for(lambda: wake) - raised = err.exception - raise raised - - task = asyncio.create_task(func()) - await asyncio.sleep(0) - # Task is waiting on the condition, cancel it there. - task.cancel(msg="foo") # type: ignore[call-arg] - with self.assertRaises(asyncio.CancelledError) as err: - await task - self.assertEqual(err.exception.args, ("foo",)) - # We should have got the _same_ exception instance as the one - # originally raised. - self.assertIs(err.exception, raised) - - @unittest.skipIf( - sys.version_info < (3, 11), "raising the same cancelled error requires Python>=3.11" - ) - async def test_cancelled_error_re_aquire(self): - # Test that a cancelled error, received when re-aquiring lock, - # will be re-raised un-modified. - wake = False - raised = None - cond = _ACondition(threading.Condition(threading.Lock())) - - async def func(): - nonlocal raised - async with cond: - with self.assertRaises(asyncio.CancelledError) as err: - await cond.wait_for(lambda: wake) - raised = err.exception - raise raised - - task = asyncio.create_task(func()) - await asyncio.sleep(0) - # Task is waiting on the condition - await cond.acquire() - wake = True - cond.notify() - await asyncio.sleep(0) - # Task is now trying to re-acquire the lock, cancel it there. - task.cancel(msg="foo") # type: ignore[call-arg] - cond.release() - with self.assertRaises(asyncio.CancelledError) as err: - await task - self.assertEqual(err.exception.args, ("foo",)) - # We should have got the _same_ exception instance as the one - # originally raised. - self.assertIs(err.exception, raised) - - @unittest.skipIf(sys.version_info < (3, 11), "asyncio.timeout requires Python>=3.11") - async def test_cancelled_wakeup(self): - # Test that a task cancelled at the "same" time as it is woken - # up as part of a Condition.notify() does not result in a lost wakeup. - # This test simulates a cancel while the target task is awaiting initial - # wakeup on the wakeup queue. - condition = _ACondition(threading.Condition(threading.Lock())) - state = 0 - - async def consumer(): - nonlocal state - async with condition: - while True: - await condition.wait_for(lambda: state != 0) - if state < 0: - return - state -= 1 - - # create two consumers - c = [asyncio.create_task(consumer()) for _ in range(2)] - # wait for them to settle - await asyncio.sleep(0.1) - async with condition: - # produce one item and wake up one - state += 1 - condition.notify(1) - - # Cancel it while it is awaiting to be run. - # This cancellation could come from the outside - c[0].cancel() - - # now wait for the item to be consumed - # if it doesn't means that our "notify" didn"t take hold. - # because it raced with a cancel() - try: - async with asyncio.timeout(1): - await condition.wait_for(lambda: state == 0) - except TimeoutError: - pass - self.assertEqual(state, 0) - - # clean up - state = -1 - condition.notify_all() - await c[1] - - @unittest.skipIf(sys.version_info < (3, 11), "asyncio.timeout requires Python>=3.11") - async def test_cancelled_wakeup_relock(self): - # Test that a task cancelled at the "same" time as it is woken - # up as part of a Condition.notify() does not result in a lost wakeup. - # This test simulates a cancel while the target task is acquiring the lock - # again. - condition = _ACondition(threading.Condition(threading.Lock())) - state = 0 - - async def consumer(): - nonlocal state - async with condition: - while True: - await condition.wait_for(lambda: state != 0) - if state < 0: - return - state -= 1 - - # create two consumers - c = [asyncio.create_task(consumer()) for _ in range(2)] - # wait for them to settle - await asyncio.sleep(0.1) - async with condition: - # produce one item and wake up one - state += 1 - condition.notify(1) - - # now we sleep for a bit. This allows the target task to wake up and - # settle on re-aquiring the lock await asyncio.sleep(0) + self.assertEqual([], result) - # Cancel it while awaiting the lock - # This cancel could come the outside. - c[0].cancel() + async with cond: + cond.notify(1) + await asyncio.sleep(1) + self.assertEqual([1], result) - # now wait for the item to be consumed - # if it doesn't means that our "notify" didn"t take hold. - # because it raced with a cancel() - try: - async with asyncio.timeout(1): - await condition.wait_for(lambda: state == 0) - except TimeoutError: - pass - self.assertEqual(state, 0) + async with cond: + cond.notify(1) + cond.notify(2048) + await asyncio.sleep(1) + self.assertEqual([1, 2, 3], result) - # clean up - state = -1 - condition.notify_all() - await c[1] + self.assertTrue(t1.done()) + self.assertTrue(t1.result()) + self.assertTrue(t2.done()) + self.assertTrue(t2.result()) + self.assertTrue(t3.done()) + self.assertTrue(t3.result()) + async def test_notify_all(self): + cond = _async_create_condition(_async_create_lock()) -class TestCondition(unittest.IsolatedAsyncioTestCase): - async def test_multiple_loops_notify(self): - cond = _ACondition(threading.Condition(threading.Lock())) + result = [] - def tmain(cond): - async def atmain(cond): - await asyncio.sleep(1) + async def c1(result): async with cond: - cond.notify(1) - - asyncio.run(atmain(cond)) - - t = threading.Thread(target=tmain, args=(cond,)) - t.start() + if await cond.wait(): + result.append(1) + return True - async with cond: - self.assertTrue(await cond.wait(30)) - t.join() - - async def test_multiple_loops_notify_all(self): - cond = _ACondition(threading.Condition(threading.Lock())) - results = [] - - def tmain(cond, results): - async def atmain(cond, results): - await asyncio.sleep(1) + async def c2(result): async with cond: - res = await cond.wait(30) - results.append(res) - - asyncio.run(atmain(cond, results)) + if await cond.wait(): + result.append(2) + return True - nthreads = 5 - threads = [] - for _ in range(nthreads): - threads.append(threading.Thread(target=tmain, args=(cond, results))) - for t in threads: - t.start() + t1 = asyncio.create_task(c1(result)) + t2 = asyncio.create_task(c2(result)) - await asyncio.sleep(2) - async with cond: - cond.notify_all() + await asyncio.sleep(0) + self.assertEqual([], result) - for t in threads: - t.join() + async with cond: + cond.notify_all() + await asyncio.sleep(1) + self.assertEqual([1, 2], result) + + self.assertTrue(t1.done()) + self.assertTrue(t1.result()) + self.assertTrue(t2.done()) + self.assertTrue(t2.result()) + + async def test_context_manager(self): + cond = _async_create_condition(_async_create_lock()) + self.assertFalse(cond.locked()) + async with cond: + self.assertTrue(cond.locked()) + self.assertFalse(cond.locked()) - self.assertEqual(results, [True] * nthreads) + async def test_timeout_in_block(self): + condition = _async_create_condition(_async_create_lock()) + async with condition: + with self.assertRaises(asyncio.TimeoutError): + await asyncio.wait_for(condition.wait(), timeout=0.5) + + @unittest.skipIf( + sys.version_info < (3, 11), "raising the same cancelled error requires Python>=3.11" + ) + async def test_cancelled_error_wakeup(self): + # Test that a cancelled error, received when awaiting wakeup, + # will be re-raised un-modified. + wake = False + raised = None + cond = _async_create_condition(_async_create_lock()) + + async def func(): + nonlocal raised + async with cond: + with self.assertRaises(asyncio.CancelledError) as err: + await cond.wait_for(lambda: wake) + raised = err.exception + raise raised + task = asyncio.create_task(func()) + await asyncio.sleep(0) + # Task is waiting on the condition, cancel it there. + task.cancel(msg="foo") # type: ignore[call-arg] + with self.assertRaises(asyncio.CancelledError) as err: + await task + self.assertEqual(err.exception.args, ("foo",)) + # We should have got the _same_ exception instance as the one + # originally raised. + self.assertIs(err.exception, raised) + + @unittest.skipIf( + sys.version_info < (3, 11), "raising the same cancelled error requires Python>=3.11" + ) + async def test_cancelled_error_re_aquire(self): + # Test that a cancelled error, received when re-aquiring lock, + # will be re-raised un-modified. + wake = False + raised = None + cond = _async_create_condition(_async_create_lock()) + + async def func(): + nonlocal raised + async with cond: + with self.assertRaises(asyncio.CancelledError) as err: + await cond.wait_for(lambda: wake) + raised = err.exception + raise raised -if __name__ == "__main__": - unittest.main() + task = asyncio.create_task(func()) + await asyncio.sleep(0) + # Task is waiting on the condition + await cond.acquire() + wake = True + cond.notify() + await asyncio.sleep(0) + # Task is now trying to re-acquire the lock, cancel it there. + task.cancel(msg="foo") # type: ignore[call-arg] + cond.release() + with self.assertRaises(asyncio.CancelledError) as err: + await task + self.assertEqual(err.exception.args, ("foo",)) + # We should have got the _same_ exception instance as the one + # originally raised. + self.assertIs(err.exception, raised) + + @unittest.skipIf(sys.version_info < (3, 11), "asyncio.timeout requires Python>=3.11") + async def test_cancelled_wakeup(self): + # Test that a task cancelled at the "same" time as it is woken + # up as part of a Condition.notify() does not result in a lost wakeup. + # This test simulates a cancel while the target task is awaiting initial + # wakeup on the wakeup queue. + condition = _async_create_condition(_async_create_lock()) + state = 0 + + async def consumer(): + nonlocal state + async with condition: + while True: + await condition.wait_for(lambda: state != 0) + if state < 0: + return + state -= 1 + + # create two consumers + c = [asyncio.create_task(consumer()) for _ in range(2)] + # wait for them to settle + await asyncio.sleep(0.1) + async with condition: + # produce one item and wake up one + state += 1 + condition.notify(1) + + # Cancel it while it is awaiting to be run. + # This cancellation could come from the outside + c[0].cancel() + + # now wait for the item to be consumed + # if it doesn't means that our "notify" didn"t take hold. + # because it raced with a cancel() + try: + async with asyncio.timeout(1): + await condition.wait_for(lambda: state == 0) + except TimeoutError: + pass + self.assertEqual(state, 0) + + # clean up + state = -1 + condition.notify_all() + await c[1] + + @unittest.skipIf(sys.version_info < (3, 11), "asyncio.timeout requires Python>=3.11") + async def test_cancelled_wakeup_relock(self): + # Test that a task cancelled at the "same" time as it is woken + # up as part of a Condition.notify() does not result in a lost wakeup. + # This test simulates a cancel while the target task is acquiring the lock + # again. + condition = _async_create_condition(_async_create_lock()) + state = 0 + + async def consumer(): + nonlocal state + async with condition: + while True: + await condition.wait_for(lambda: state != 0) + if state < 0: + return + state -= 1 + + # create two consumers + c = [asyncio.create_task(consumer()) for _ in range(2)] + # wait for them to settle + await asyncio.sleep(0.1) + async with condition: + # produce one item and wake up one + state += 1 + condition.notify(1) + + # now we sleep for a bit. This allows the target task to wake up and + # settle on re-aquiring the lock + await asyncio.sleep(0) + + # Cancel it while awaiting the lock + # This cancel could come the outside. + c[0].cancel() + + # now wait for the item to be consumed + # if it doesn't means that our "notify" didn"t take hold. + # because it raced with a cancel() + try: + async with asyncio.timeout(1): + await condition.wait_for(lambda: state == 0) + except TimeoutError: + pass + self.assertEqual(state, 0) + + # clean up + state = -1 + condition.notify_all() + await c[1] + + if __name__ == "__main__": + unittest.main() diff --git a/tools/synchro.py b/tools/synchro.py index f460b348c4..17841d3025 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -114,6 +114,9 @@ "async_wait_for_event": "wait_for_event", "pymongo_server_monitor_task": "pymongo_server_monitor_thread", "pymongo_server_rtt_task": "pymongo_server_rtt_thread", + "_async_create_lock": "_create_lock", + "_async_create_condition": "_create_condition", + "_async_cond_wait": "_cond_wait", } docstring_replacements: dict[tuple[str, str], str] = { @@ -134,8 +137,6 @@ ".. warning:: This API is currently in beta, meaning the classes, methods, and behaviors described within may change before the full release." } -type_replacements = {"_Condition": "threading.Condition"} - import_replacements = {"test.synchronous": "test"} _pymongo_base = "./pymongo/asynchronous/" @@ -236,8 +237,6 @@ def process_files(files: list[str]) -> None: lines = translate_async_sleeps(lines) if file in docstring_translate_files: lines = translate_docstrings(lines) - translate_locks(lines) - translate_types(lines) if file in sync_test_files: translate_imports(lines) f.seek(0) @@ -271,34 +270,6 @@ def translate_coroutine_types(lines: list[str]) -> list[str]: return lines -def translate_locks(lines: list[str]) -> list[str]: - lock_lines = [line for line in lines if "_Lock(" in line] - cond_lines = [line for line in lines if "_Condition(" in line] - for line in lock_lines: - res = re.search(r"_Lock\(([^()]*\([^()]*\))\)", line) - if res: - old = res[0] - index = lines.index(line) - lines[index] = line.replace(old, res[1]) - for line in cond_lines: - res = re.search(r"_Condition\(([^()]*\([^()]*\))\)", line) - if res: - old = res[0] - index = lines.index(line) - lines[index] = line.replace(old, res[1]) - - return lines - - -def translate_types(lines: list[str]) -> list[str]: - for k, v in type_replacements.items(): - matches = [line for line in lines if k in line and "import" not in line] - for line in matches: - index = lines.index(line) - lines[index] = line.replace(k, v) - return lines - - def translate_imports(lines: list[str]) -> list[str]: for k, v in import_replacements.items(): matches = [line for line in lines if k in line and "import" in line] From 054db7c6965d418ede8ce4f5fe253530c3d7e211 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 18 Oct 2024 15:01:00 -0400 Subject: [PATCH 06/14] Sync with master again (#1950) Signed-off-by: dependabot[bot] Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com> Co-authored-by: Steven Silvester Co-authored-by: Shane Harvey Co-authored-by: Jeffrey A. Clark Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jib --- .evergreen/config.yml | 1539 +++++++++++++---- .evergreen/hatch.sh | 4 +- .evergreen/run-tests.sh | 4 +- .evergreen/scripts/generate_config.py | 310 +++- doc/changelog.rst | 9 + pymongo/asynchronous/bulk.py | 13 + pymongo/asynchronous/client_bulk.py | 9 + pymongo/asynchronous/collection.py | 25 + pymongo/network_layer.py | 5 +- pymongo/operations.py | 57 +- pymongo/synchronous/bulk.py | 13 + pymongo/synchronous/client_bulk.py | 9 + pymongo/synchronous/collection.py | 25 + pyproject.toml | 1 + requirements/typing.txt | 2 +- test/asynchronous/test_auth.py | 4 + test/asynchronous/test_bulk.py | 6 + test/asynchronous/test_change_stream.py | 5 +- test/asynchronous/test_collation.py | 5 +- test/asynchronous/test_collection.py | 3 +- .../test_collection_management.py | 41 + test/asynchronous/test_create_entities.py | 128 ++ test/asynchronous/test_cursor.py | 4 +- test/asynchronous/test_grid_file.py | 4 +- test/asynchronous/test_monitoring.py | 29 +- test/asynchronous/test_session.py | 3 +- test/asynchronous/unified_format.py | 2 +- test/auth_oidc/test_auth_oidc.py | 6 +- .../aggregate-write-readPreference.json | 69 - .../unified/bulkWrite-replaceOne-sort.json | 239 +++ .../unified/bulkWrite-updateOne-sort.json | 255 +++ .../client-bulkWrite-partialResults.json | 540 ++++++ .../client-bulkWrite-replaceOne-sort.json | 162 ++ .../client-bulkWrite-updateOne-sort.json | 166 ++ .../db-aggregate-write-readPreference.json | 51 - test/crud/unified/replaceOne-sort.json | 232 +++ test/crud/unified/updateOne-sort.json | 240 +++ test/test_auth.py | 4 + test/test_bulk.py | 6 + test/test_change_stream.py | 5 +- test/test_collation.py | 5 +- test/test_collection.py | 3 +- test/test_collection_management.py | 12 +- test/test_create_entities.py | 2 + test/test_cursor.py | 4 +- test/test_grid_file.py | 4 +- test/test_index_management.py | 4 +- test/test_monitoring.py | 29 +- test/test_operations.py | 80 + test/test_read_write_concern_spec.py | 6 +- test/test_server_selection.py | 3 +- test/test_session.py | 3 +- test/test_ssl.py | 1 + test/unified_format.py | 2 +- test/utils.py | 4 - tools/synchro.py | 2 + 56 files changed, 3878 insertions(+), 520 deletions(-) create mode 100644 test/asynchronous/test_collection_management.py create mode 100644 test/asynchronous/test_create_entities.py create mode 100644 test/crud/unified/bulkWrite-replaceOne-sort.json create mode 100644 test/crud/unified/bulkWrite-updateOne-sort.json create mode 100644 test/crud/unified/client-bulkWrite-partialResults.json create mode 100644 test/crud/unified/client-bulkWrite-replaceOne-sort.json create mode 100644 test/crud/unified/client-bulkWrite-updateOne-sort.json create mode 100644 test/crud/unified/replaceOne-sort.json create mode 100644 test/crud/unified/updateOne-sort.json create mode 100644 test/test_operations.py diff --git a/.evergreen/config.yml b/.evergreen/config.yml index dee4b608ec..9083da145b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -409,6 +409,7 @@ functions: AUTH=${AUTH} \ SSL=${SSL} \ TEST_DATA_LAKE=${TEST_DATA_LAKE} \ + TEST_SUITES=${TEST_SUITES} \ MONGODB_API_VERSION=${MONGODB_API_VERSION} \ SKIP_HATCH=${SKIP_HATCH} \ bash ${PROJECT_DIRECTORY}/.evergreen/hatch.sh test:test-eg @@ -2111,23 +2112,6 @@ axes: AUTH: "noauth" SSL: "nossl" - # Choice of wire protocol compression support - - id: compression - display_name: Compression - values: - - id: snappy - display_name: snappy compression - variables: - COMPRESSORS: "snappy" - - id: zlib - display_name: zlib compression - variables: - COMPRESSORS: "zlib" - - id: zstd - display_name: zstd compression - variables: - COMPRESSORS: "zstd" - # Choice of MongoDB server version - id: mongodb-version display_name: "MongoDB" @@ -2321,42 +2305,6 @@ axes: variables: COVERAGE: "coverage" - # Run encryption tests? - - id: encryption - display_name: "Encryption" - values: - - id: "encryption" - display_name: "Encryption" - tags: ["encryption_tag"] - variables: - test_encryption: true - batchtime: 10080 # 7 days - - id: "encryption_pyopenssl" - display_name: "Encryption PyOpenSSL" - tags: ["encryption_tag"] - variables: - test_encryption: true - test_encryption_pyopenssl: true - batchtime: 10080 # 7 days - # The path to crypt_shared is stored in the $CRYPT_SHARED_LIB_PATH expansion. - - id: "encryption_crypt_shared" - display_name: "Encryption shared lib" - tags: ["encryption_tag"] - variables: - test_encryption: true - test_crypt_shared: true - batchtime: 10080 # 7 days - - # Run pyopenssl tests? - - id: pyopenssl - display_name: "PyOpenSSL" - values: - - id: "enabled" - display_name: "PyOpenSSL" - variables: - test_pyopenssl: true - batchtime: 10080 # 7 days - - id: versionedApi display_name: "versionedApi" values: @@ -2379,16 +2327,6 @@ axes: variables: ORCHESTRATION_FILE: "versioned-api-testing.json" - # Run load balancer tests? - - id: loadbalancer - display_name: "Load Balancer" - values: - - id: "enabled" - display_name: "Load Balancer" - variables: - test_loadbalancer: true - batchtime: 10080 # 7 days - - id: serverless display_name: "Serverless" values: @@ -2399,80 +2337,1044 @@ axes: batchtime: 10080 # 7 days buildvariants: -- matrix_name: "tests-fips" - matrix_spec: - platform: - - rhel9-fips - auth: "auth" - ssl: "ssl" - display_name: "${platform} ${auth} ${ssl}" +# Server Tests for RHEL8. +- name: test-rhel8-py3.9-auth-ssl-cov tasks: - - "test-fips-standalone" + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 Auth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.9-noauth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 NoAuth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.9-noauth-nossl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 NoAuth NoSSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-auth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 Auth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-noauth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 NoAuth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-noauth-nossl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 NoAuth NoSSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-auth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 Auth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-noauth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 NoAuth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-noauth-nossl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 NoAuth NoSSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.10-auth-ssl + tasks: + - name: .standalone + display_name: Test RHEL8 py3.10 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: test-rhel8-py3.11-noauth-ssl + tasks: + - name: .replica_set + display_name: Test RHEL8 py3.11 NoAuth SSL + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: test-rhel8-py3.12-noauth-nossl + tasks: + - name: .sharded_cluster + display_name: Test RHEL8 py3.12 NoAuth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: test-rhel8-pypy3.9-auth-ssl + tasks: + - name: .standalone + display_name: Test RHEL8 pypy3.9 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- matrix_name: "test-macos" - matrix_spec: - platform: - # MacOS introduced SSL support with MongoDB >= 3.2. - # Older server versions (2.6, 3.0) are supported without SSL. - - macos - auth: "*" - ssl: "*" - exclude_spec: - # No point testing with SSL without auth. - - platform: macos - auth: "noauth" - ssl: "ssl" - display_name: "${platform} ${auth} ${ssl}" +# Server tests for MacOS. +- name: test-macos-py3.9-auth-ssl-sync tasks: - - ".latest" - - ".8.0" - - ".7.0" - - ".6.0" - - ".5.0" - - ".4.4" - - ".4.2" - - ".4.0" + - name: .standalone + display_name: Test macOS py3.9 Auth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + SKIP_CSOT_TESTS: "true" +- name: test-macos-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 Auth SSL Async + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + SKIP_CSOT_TESTS: "true" +- name: test-macos-py3.13-noauth-ssl-sync + tasks: + - name: .replica_set + display_name: Test macOS py3.13 NoAuth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + SKIP_CSOT_TESTS: "true" +- name: test-macos-py3.13-noauth-ssl-async + tasks: + - name: .replica_set + display_name: Test macOS py3.13 NoAuth SSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + SKIP_CSOT_TESTS: "true" +- name: test-macos-py3.9-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.9 NoAuth NoSSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + SKIP_CSOT_TESTS: "true" +- name: test-macos-py3.9-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.9 NoAuth NoSSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + SKIP_CSOT_TESTS: "true" -- matrix_name: "test-macos-arm64" - matrix_spec: - platform: - - macos-arm64 - auth-ssl: "*" - display_name: "${platform} ${auth-ssl}" +# Server tests for macOS Arm64. +- name: test-macos-arm64-py3.9-auth-ssl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 Auth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-auth-ssl-async + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 Auth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.13-noauth-ssl-sync + tasks: + - name: .replica_set .6.0 + - name: .replica_set .7.0 + - name: .replica_set .8.0 + - name: .replica_set .rapid + - name: .replica_set .latest + display_name: Test macOS Arm64 py3.13 NoAuth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-ssl-async + tasks: + - name: .replica_set .6.0 + - name: .replica_set .7.0 + - name: .replica_set .8.0 + - name: .replica_set .rapid + - name: .replica_set .latest + display_name: Test macOS Arm64 py3.13 NoAuth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-sync + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-async + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + +# Server tests for Windows. +- name: test-win64-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: C:/python/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win64-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win64-py3.13-noauth-ssl-sync + tasks: + - name: .replica_set + display_name: Test Win64 py3.13 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: C:/python/Python313/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win64-py3.13-noauth-ssl-async + tasks: + - name: .replica_set + display_name: Test Win64 py3.13 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/Python313/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win64-py3.9-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.9 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + PYTHON_BINARY: C:/python/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win64-py3.9-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.9 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win32-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: C:/python/32/Python39/python.exe + SKIP_CSOT_TESTS: "true" + +# Server tests for Win32. +- name: test-win32-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/32/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win32-py3.13-noauth-ssl-sync + tasks: + - name: .replica_set + display_name: Test Win32 py3.13 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + PYTHON_BINARY: C:/python/32/Python313/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win32-py3.13-noauth-ssl-async + tasks: + - name: .replica_set + display_name: Test Win32 py3.13 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/32/Python313/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win32-py3.9-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.9 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + PYTHON_BINARY: C:/python/32/Python39/python.exe + SKIP_CSOT_TESTS: "true" +- name: test-win32-py3.9-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.9 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + PYTHON_BINARY: C:/python/32/Python39/python.exe + SKIP_CSOT_TESTS: "true" + +# Encryption tests. +- name: encryption-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-py3.10-auth-ssl + tasks: + - name: .replica_set + display_name: Encryption RHEL8 py3.10 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: encryption-crypt_shared-rhel8-py3.11-auth-nossl + tasks: + - name: .replica_set + display_name: Encryption crypt_shared RHEL8 py3.11 Auth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: encryption-pyopenssl-rhel8-py3.12-auth-ssl + tasks: + - name: .replica_set + display_name: Encryption PyOpenSSL RHEL8 py3.12 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + TEST_ENCRYPTION_PYOPENSSL: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: encryption-rhel8-pypy3.9-auth-nossl + tasks: + - name: .replica_set + display_name: Encryption RHEL8 pypy3.9 Auth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: encryption-macos-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption macOS py3.9 Auth SSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-macos-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption macOS py3.13 Auth NoSSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-macos-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared macOS py3.9 Auth SSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-macos-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared macOS py3.13 Auth NoSSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-win64-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption Win64 py3.9 Auth SSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: C:/python/Python39/python.exe + tags: [encryption_tag] +- name: encryption-win64-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption Win64 py3.13 Auth NoSSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: C:/python/Python313/python.exe + tags: [encryption_tag] +- name: encryption-crypt_shared-win64-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared Win64 py3.9 Auth SSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: C:/python/Python39/python.exe + tags: [encryption_tag] +- name: encryption-crypt_shared-win64-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared Win64 py3.13 Auth NoSSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: C:/python/Python313/python.exe + tags: [encryption_tag] + +# Compressor tests. +- name: snappy-compression-rhel8-py3.9-no-c + tasks: + - name: .standalone + display_name: snappy compression RHEL8 py3.9 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: snappy-compression-rhel8-py3.10 + tasks: + - name: .standalone + display_name: snappy compression RHEL8 py3.10 + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: zlib-compression-rhel8-py3.11-no-c + tasks: + - name: .standalone + display_name: zlib compression RHEL8 py3.11 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: zlib-compression-rhel8-py3.12 + tasks: + - name: .standalone + display_name: zlib compression RHEL8 py3.12 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: zstd-compression-rhel8-py3.13-no-c + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 py3.13 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: zstd-compression-rhel8-py3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: snappy-compression-rhel8-pypy3.9 + tasks: + - name: .standalone + display_name: snappy compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: zlib-compression-rhel8-pypy3.10 + tasks: + - name: .standalone + display_name: zlib compression RHEL8 pypy3.10 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: zstd-compression-rhel8-pypy3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 + +# Enterprise auth tests. +- name: enterprise-auth-macos-py3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth macOS py3.9 Auth + run_on: + - macos-14 + expansions: + AUTH: auth + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: enterprise-auth-rhel8-py3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: enterprise-auth-rhel8-py3.11-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.11 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: enterprise-auth-rhel8-py3.12-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.12 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: enterprise-auth-win64-py3.13-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth Win64 py3.13 Auth + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + PYTHON_BINARY: C:/python/Python313/python.exe +- name: enterprise-auth-rhel8-pypy3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: enterprise-auth-rhel8-pypy3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + +# PyOpenSSL tests. +- name: pyopenssl-macos-py3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL macOS py3.9 + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: noauth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: pyopenssl-rhel8-py3.10 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: pyopenssl-rhel8-py3.11 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.11 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: pyopenssl-rhel8-py3.12 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.12 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: pyopenssl-win64-py3.13 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL Win64 py3.13 + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: C:/python/Python313/python.exe +- name: pyopenssl-rhel8-pypy3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.9 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: pyopenssl-rhel8-pypy3.10 tasks: - - ".latest" - - ".8.0" - - ".7.0" - - ".6.0" - - ".5.0" - - ".4.4" + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- matrix_name: "test-macos-encryption" +- matrix_name: "tests-fips" matrix_spec: platform: - - macos + - rhel9-fips auth: "auth" - ssl: "nossl" - encryption: "*" - display_name: "${encryption} ${platform} ${auth} ${ssl}" - tasks: "test-latest-replica_set" - rules: - - if: - encryption: ["encryption", "encryption_crypt_shared"] - platform: macos - auth: "auth" - ssl: "nossl" - then: - add_tasks: &encryption-server-versions - - ".rapid" - - ".latest" - - ".8.0" - - ".7.0" - - ".6.0" - - ".5.0" - - ".4.4" - - ".4.2" - - ".4.0" + ssl: "ssl" + display_name: "${platform} ${auth} ${ssl}" + tasks: + - "test-fips-standalone" # Test one server version with zSeries, POWER8, and ARM. - matrix_name: "test-different-cpu-architectures" @@ -2486,85 +3388,6 @@ buildvariants: tasks: - ".6.0" -- matrix_name: "tests-python-version-rhel8-test-ssl" - matrix_spec: - platform: rhel8 - python-version: "*" - auth-ssl: "*" - coverage: "*" - display_name: "${python-version} ${platform} ${auth-ssl} ${coverage}" - tasks: &all-server-versions - - ".rapid" - - ".latest" - - ".8.0" - - ".7.0" - - ".6.0" - - ".5.0" - - ".4.4" - - ".4.2" - - ".4.0" - -- matrix_name: "tests-pyopenssl" - matrix_spec: - platform: rhel8 - python-version: "*" - auth: "*" - ssl: "ssl" - pyopenssl: "*" - # Only test "noauth" with Python 3.9. - exclude_spec: - platform: rhel8 - python-version: ["3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10"] - auth: "noauth" - ssl: "ssl" - pyopenssl: "*" - display_name: "PyOpenSSL ${platform} ${python-version} ${auth}" - tasks: - - '.replica_set' - # Test standalone and sharded only on 7.0. - - '.7.0' - -- matrix_name: "tests-pyopenssl-macOS" - matrix_spec: - platform: macos - auth: "auth" - ssl: "ssl" - pyopenssl: "*" - display_name: "PyOpenSSL ${platform} ${auth}" - tasks: - - '.replica_set' - -- matrix_name: "tests-pyopenssl-windows" - matrix_spec: - platform: windows - python-version-windows: "*" - auth: "auth" - ssl: "ssl" - pyopenssl: "*" - display_name: "PyOpenSSL ${platform} ${python-version-windows} ${auth}" - tasks: - - '.replica_set' - -- matrix_name: "tests-python-version-rhel8-test-encryption" - matrix_spec: - platform: rhel8 - python-version: "*" - auth-ssl: noauth-nossl -# TODO: dependency error for 'coverage-report' task: -# dependency tests-python-version-rhel62-test-encryption_.../test-2.6-standalone is not present in the project config -# coverage: "*" - encryption: "*" - display_name: "${encryption} ${python-version} ${platform} ${auth-ssl}" - tasks: "test-latest-replica_set" - rules: - - if: - encryption: ["encryption", "encryption_crypt_shared"] - platform: rhel8 - auth-ssl: noauth-nossl - python-version: "*" - then: - add_tasks: *encryption-server-versions - - matrix_name: "tests-python-version-rhel8-without-c-extensions" matrix_spec: platform: rhel8 @@ -2580,39 +3403,16 @@ buildvariants: auth-ssl: "*" coverage: "*" display_name: "${c-extensions} ${python-version} ${platform} ${auth} ${ssl} ${coverage}" - tasks: *all-server-versions - -- matrix_name: "tests-python-version-rhel8-compression" - matrix_spec: - platform: rhel8 - python-version: "*" - c-extensions: "*" - compression: "*" - exclude_spec: - # These interpreters are always tested without extensions. - - platform: rhel8 - python-version: ["pypy3.9", "pypy3.10"] - c-extensions: "with-c-extensions" - compression: "*" - display_name: "${compression} ${c-extensions} ${python-version} ${platform}" - tasks: - - "test-latest-standalone" - - "test-8.0-standalone" - - "test-7.0-standalone" - - "test-6.0-standalone" - - "test-5.0-standalone" - - "test-4.4-standalone" - - "test-4.2-standalone" - - "test-4.0-standalone" - rules: - # Server version 4.0 supports snappy and zlib but not zstd. - - if: - python-version: "*" - c-extensions: "*" - compression: ["zstd"] - then: - remove_tasks: - - "test-4.0-standalone" + tasks: &all-server-versions + - ".rapid" + - ".latest" + - ".8.0" + - ".7.0" + - ".6.0" + - ".5.0" + - ".4.4" + - ".4.2" + - ".4.0" - matrix_name: "tests-python-version-green-framework-rhel8" matrix_spec: @@ -2629,22 +3429,6 @@ buildvariants: display_name: "${green-framework} ${python-version} ${platform} ${auth-ssl}" tasks: *all-server-versions -- matrix_name: "tests-windows-python-version" - matrix_spec: - platform: windows - python-version-windows: "*" - auth-ssl: "*" - display_name: "${platform} ${python-version-windows} ${auth-ssl}" - tasks: *all-server-versions - -- matrix_name: "tests-windows-python-version-32-bit" - matrix_spec: - platform: windows - python-version-windows-32: "*" - auth-ssl: "*" - display_name: "${platform} ${python-version-windows-32} ${auth-ssl}" - tasks: *all-server-versions - - matrix_name: "tests-python-version-supports-openssl-102-test-ssl" matrix_spec: platform: rhel7 @@ -2655,23 +3439,6 @@ buildvariants: tasks: - ".5.0" -- matrix_name: "tests-windows-encryption" - matrix_spec: - platform: windows - python-version-windows: "*" - auth-ssl: "*" - encryption: "*" - display_name: "${encryption} ${platform} ${python-version-windows} ${auth-ssl}" - tasks: "test-latest-replica_set" - rules: - - if: - encryption: ["encryption", "encryption_crypt_shared"] - platform: windows - python-version-windows: "*" - auth-ssl: "*" - then: - add_tasks: *encryption-server-versions - # Storage engine tests on RHEL 8.4 (x86_64) with Python 3.9. - matrix_name: "tests-storage-engines" matrix_spec: @@ -2714,24 +3481,6 @@ buildvariants: tasks: - ".latest" -- matrix_name: "test-linux-enterprise-auth" - matrix_spec: - platform: rhel8 - python-version: "*" - auth: "auth" - display_name: "Enterprise ${auth} ${platform} ${python-version}" - tasks: - - name: "test-enterprise-auth" - -- matrix_name: "tests-windows-enterprise-auth" - matrix_spec: - platform: windows - python-version-windows: "*" - auth: "auth" - display_name: "Enterprise ${auth} ${platform} ${python-version-windows}" - tasks: - - name: "test-enterprise-auth" - - matrix_name: "test-search-index-helpers" matrix_spec: platform: rhel8 @@ -2971,6 +3720,203 @@ buildvariants: VERSION: "8.0" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +# Load balancer tests +- name: load-balancer-rhel8-v6.0-py3.9-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: load-balancer-rhel8-v6.0-py3.10-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.10 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: load-balancer-rhel8-v6.0-py3.11-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.11 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: load-balancer-rhel8-v7.0-py3.12-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 py3.12 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: load-balancer-rhel8-v7.0-py3.13-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 py3.13 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: load-balancer-rhel8-v7.0-pypy3.9-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 pypy3.9 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: load-balancer-rhel8-v8.0-pypy3.10-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: load-balancer-rhel8-v8.0-py3.9-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 py3.9 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: load-balancer-rhel8-v8.0-py3.10-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 py3.10 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: load-balancer-rhel8-latest-py3.11-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.11 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: load-balancer-rhel8-latest-py3.12-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.12 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: load-balancer-rhel8-latest-py3.13-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.13 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: load-balancer-rhel8-rapid-pypy3.9-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid pypy3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: load-balancer-rhel8-rapid-pypy3.10-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid pypy3.10 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: load-balancer-rhel8-rapid-py3.9-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid py3.9 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + - matrix_name: "oidc-auth-test" matrix_spec: platform: [ rhel8, macos, windows ] @@ -3034,17 +3980,6 @@ buildvariants: - name: "aws-auth-test-rapid" - name: "aws-auth-test-latest" -- matrix_name: "load-balancer" - matrix_spec: - platform: rhel8 - mongodb-version: ["6.0", "7.0", "8.0", "rapid", "latest"] - auth-ssl: "*" - python-version: "*" - loadbalancer: "*" - display_name: "Load Balancer ${platform} ${python-version} ${mongodb-version} ${auth-ssl}" - tasks: - - name: "load-balancer-test" - - name: testgcpkms-variant display_name: "GCP KMS" run_on: diff --git a/.evergreen/hatch.sh b/.evergreen/hatch.sh index 6f3d36b389..45d5113cd6 100644 --- a/.evergreen/hatch.sh +++ b/.evergreen/hatch.sh @@ -34,8 +34,8 @@ else # Set up virtualenv before installing hatch fi export HATCH_CONFIG hatch config restore - hatch config set dirs.data ".hatch/data" - hatch config set dirs.cache ".hatch/cache" + hatch config set dirs.data "$(pwd)/.hatch/data" + hatch config set dirs.cache "$(pwd)/.hatch/cache" run_hatch() { python -m hatch run "$@" diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 5e8429dd28..36fa76e317 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -30,7 +30,7 @@ set -o xtrace AUTH=${AUTH:-noauth} SSL=${SSL:-nossl} -TEST_SUITES="" +TEST_SUITES=${TEST_SUITES:-} TEST_ARGS="${*:1}" export PIP_QUIET=1 # Quiet by default @@ -90,6 +90,8 @@ if [ -n "$TEST_ENTERPRISE_AUTH" ]; then export GSSAPI_HOST=${SASL_HOST} export GSSAPI_PORT=${SASL_PORT} export GSSAPI_PRINCIPAL=${PRINCIPAL} + + export TEST_SUITES="auth" fi if [ -n "$TEST_LOADBALANCER" ]; then diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index e98e527b72..6d614a9afe 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -23,10 +23,23 @@ ############## ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"] +VERSIONS_6_0_PLUS = ["6.0", "7.0", "8.0", "rapid", "latest"] CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] PYPYS = ["pypy3.9", "pypy3.10"] ALL_PYTHONS = CPYTHONS + PYPYS +MIN_MAX_PYTHON = [CPYTHONS[0], CPYTHONS[-1]] BATCHTIME_WEEK = 10080 +AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")] +TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"] +C_EXTS = ["with_ext", "without_ext"] +SYNCS = ["sync", "async"] +DISPLAY_LOOKUP = dict( + ssl=dict(ssl="SSL", nossl="NoSSL"), + auth=dict(auth="Auth", noauth="NoAuth"), + test_suites=dict(default="Sync", default_async="Async"), + coverage=dict(coverage="cov"), + no_ext={"1": "No C"}, +) HOSTS = dict() @@ -39,7 +52,9 @@ class Host: HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8") HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64") +HOSTS["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32") HOSTS["macos"] = Host("macos", "macos-14", "macOS") +HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64") ############## @@ -80,10 +95,8 @@ def create_variant( def get_python_binary(python: str, host: str) -> str: """Get the appropriate python binary given a python version and host.""" - if host == "win64": - is_32 = python.startswith("32-bit") - if is_32: - _, python = python.split() + if host in ["win64", "win32"]: + if host == "win32": base = "C:/python/32" else: base = "C:/python" @@ -93,19 +106,31 @@ def get_python_binary(python: str, host: str) -> str: if host == "rhel8": return f"/opt/python/{python}/bin/python3" - if host == "macos": + if host in ["macos", "macos-arm64"]: return f"/Library/Frameworks/Python.Framework/Versions/{python}/bin/python3" raise ValueError(f"no match found for python {python} on {host}") -def get_display_name(base: str, host: str, version: str, python: str) -> str: +def get_display_name(base: str, host: str, **kwargs) -> str: """Get the display name of a variant.""" - if version not in ["rapid", "latest"]: - version = f"v{version}" - if not python.startswith("pypy"): - python = f"py{python}" - return f"{base} {HOSTS[host].display_name} {version} {python}" + display_name = f"{base} {HOSTS[host].display_name}" + version = kwargs.pop("VERSION", None) + if version: + if version not in ["rapid", "latest"]: + version = f"v{version}" + display_name = f"{display_name} {version}" + for key, value in kwargs.items(): + name = value + if key.lower() == "python": + if not value.startswith("pypy"): + name = f"py{value}" + elif key.lower() in DISPLAY_LOOKUP: + name = DISPLAY_LOOKUP[key.lower()][value] + else: + continue + display_name = f"{display_name} {name}" + return display_name def zip_cycle(*iterables, empty_default=None): @@ -115,6 +140,21 @@ def zip_cycle(*iterables, empty_default=None): yield tuple(next(i, empty_default) for i in cycles) +def handle_c_ext(c_ext, expansions): + """Handle c extension option.""" + if c_ext == C_EXTS[0]: + expansions["NO_EXT"] = "1" + + +def generate_yaml(tasks=None, variants=None): + """Generate the yaml for a given set of tasks and variants.""" + project = EvgProject(tasks=tasks, buildvariants=variants) + out = ShrubService.generate_yaml(project) + # Dedent by two spaces to match what we use in config.yml + lines = [line[2:] for line in out.splitlines()] + print("\n".join(lines)) # noqa: T201 + + ############## # Variants ############## @@ -159,9 +199,253 @@ def create_ocsp_variants() -> list[BuildVariant]: return variants +def create_server_variants() -> list[BuildVariant]: + variants = [] + + # Run the full matrix on linux with min and max CPython, and latest pypy. + host = "rhel8" + for python, (auth, ssl) in product([*MIN_MAX_PYTHON, PYPYS[-1]], AUTH_SSLS): + display_name = f"Test {host}" + expansions = dict(AUTH=auth, SSL=ssl, COVERAGE="coverage") + display_name = get_display_name("Test", host, python=python, **expansions) + variant = create_variant( + [f".{t}" for t in TOPOLOGIES], + display_name, + python=python, + host=host, + tags=["coverage_tag"], + expansions=expansions, + ) + variants.append(variant) + + # Test the rest of the pythons on linux. + for python, (auth, ssl), topology in zip_cycle( + CPYTHONS[1:-1] + PYPYS[:-1], AUTH_SSLS, TOPOLOGIES + ): + display_name = f"Test {host}" + expansions = dict(AUTH=auth, SSL=ssl) + display_name = get_display_name("Test", host, python=python, **expansions) + variant = create_variant( + [f".{topology}"], + display_name, + python=python, + host=host, + expansions=expansions, + ) + variants.append(variant) + + # Test a subset on each of the other platforms. + for host in ("macos", "macos-arm64", "win64", "win32"): + for (python, (auth, ssl), topology), sync in product( + zip_cycle(MIN_MAX_PYTHON, AUTH_SSLS, TOPOLOGIES), SYNCS + ): + test_suite = "default" if sync == "sync" else "default_async" + tasks = [f".{topology}"] + # MacOS arm64 only works on server versions 6.0+ + if host == "macos-arm64": + tasks = [f".{topology} .{version}" for version in VERSIONS_6_0_PLUS] + expansions = dict(AUTH=auth, SSL=ssl, TEST_SUITES=test_suite, SKIP_CSOT_TESTS="true") + display_name = get_display_name("Test", host, python=python, **expansions) + variant = create_variant( + tasks, + display_name, + python=python, + host=host, + expansions=expansions, + ) + variants.append(variant) + + return variants + + +def create_encryption_variants() -> list[BuildVariant]: + variants = [] + tags = ["encryption_tag"] + batchtime = BATCHTIME_WEEK + + def get_encryption_expansions(encryption, ssl="ssl"): + expansions = dict(AUTH="auth", SSL=ssl, test_encryption="true") + if "crypt_shared" in encryption: + expansions["test_crypt_shared"] = "true" + if "PyOpenSSL" in encryption: + expansions["test_encryption_pyopenssl"] = "true" + return expansions + + host = "rhel8" + + # Test against all server versions and topolgies for the three main python versions. + encryptions = ["Encryption", "Encryption crypt_shared", "Encryption PyOpenSSL"] + for encryption, python in product(encryptions, [*MIN_MAX_PYTHON, PYPYS[-1]]): + expansions = get_encryption_expansions(encryption) + display_name = get_display_name(encryption, host, python=python, **expansions) + variant = create_variant( + [f".{t}" for t in TOPOLOGIES], + display_name, + python=python, + host=host, + expansions=expansions, + batchtime=batchtime, + tags=tags, + ) + variants.append(variant) + + # Test the rest of the pythons on linux for all server versions. + for encryption, python, ssl in zip_cycle( + encryptions, CPYTHONS[1:-1] + PYPYS[:-1], ["ssl", "nossl"] + ): + expansions = get_encryption_expansions(encryption, ssl) + display_name = get_display_name(encryption, host, python=python, **expansions) + variant = create_variant( + [".replica_set"], + display_name, + python=python, + host=host, + expansions=expansions, + ) + variants.append(variant) + + # Test on macos and linux on one server version and topology for min and max python. + encryptions = ["Encryption", "Encryption crypt_shared"] + task_names = [".latest .replica_set"] + for host, encryption, python in product(["macos", "win64"], encryptions, MIN_MAX_PYTHON): + ssl = "ssl" if python == CPYTHONS[0] else "nossl" + expansions = get_encryption_expansions(encryption, ssl) + display_name = get_display_name(encryption, host, python=python, **expansions) + variant = create_variant( + task_names, + display_name, + python=python, + host=host, + expansions=expansions, + batchtime=batchtime, + tags=tags, + ) + variants.append(variant) + return variants + + +def create_load_balancer_variants(): + # Load balancer tests - run all supported versions for all combinations of auth and ssl and system python. + host = "rhel8" + task_names = ["load-balancer-test"] + batchtime = BATCHTIME_WEEK + expansions_base = dict(test_loadbalancer="true") + versions = ["6.0", "7.0", "8.0", "latest", "rapid"] + variants = [] + pythons = CPYTHONS + PYPYS + for ind, (version, (auth, ssl)) in enumerate(product(versions, AUTH_SSLS)): + expansions = dict(VERSION=version, AUTH=auth, SSL=ssl) + expansions.update(expansions_base) + python = pythons[ind % len(pythons)] + display_name = get_display_name("Load Balancer", host, python=python, **expansions) + variant = create_variant( + task_names, + display_name, + python=python, + host=host, + expansions=expansions, + batchtime=batchtime, + ) + variants.append(variant) + return variants + + +def create_compression_variants(): + # Compression tests - standalone versions of each server, across python versions, with and without c extensions. + # PyPy interpreters are always tested without extensions. + host = "rhel8" + task_names = dict(snappy=[".standalone"], zlib=[".standalone"], zstd=[".standalone !.4.0"]) + variants = [] + for ind, (compressor, c_ext) in enumerate(product(["snappy", "zlib", "zstd"], C_EXTS)): + expansions = dict(COMPRESSORS=compressor) + handle_c_ext(c_ext, expansions) + base_name = f"{compressor} compression" + python = CPYTHONS[ind % len(CPYTHONS)] + display_name = get_display_name(base_name, host, python=python, **expansions) + variant = create_variant( + task_names[compressor], + display_name, + python=python, + host=host, + expansions=expansions, + ) + variants.append(variant) + + other_pythons = PYPYS + CPYTHONS[ind:] + for compressor, python in zip_cycle(["snappy", "zlib", "zstd"], other_pythons): + expansions = dict(COMPRESSORS=compressor) + handle_c_ext(c_ext, expansions) + base_name = f"{compressor} compression" + display_name = get_display_name(base_name, host, python=python, **expansions) + variant = create_variant( + task_names[compressor], + display_name, + python=python, + host=host, + expansions=expansions, + ) + variants.append(variant) + + return variants + + +def create_enterprise_auth_variants(): + expansions = dict(AUTH="auth") + variants = [] + + # All python versions across platforms. + for python in ALL_PYTHONS: + if python == CPYTHONS[0]: + host = "macos" + elif python == CPYTHONS[-1]: + host = "win64" + else: + host = "rhel8" + display_name = get_display_name("Enterprise Auth", host, python=python, **expansions) + variant = create_variant( + ["test-enterprise-auth"], display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + + return variants + + +def create_pyopenssl_variants(): + base_name = "PyOpenSSL" + batchtime = BATCHTIME_WEEK + base_expansions = dict(test_pyopenssl="true", SSL="ssl") + variants = [] + + for python in ALL_PYTHONS: + # Only test "noauth" with min python. + auth = "noauth" if python == CPYTHONS[0] else "auth" + if python == CPYTHONS[0]: + host = "macos" + elif python == CPYTHONS[-1]: + host = "win64" + else: + host = "rhel8" + expansions = dict(AUTH=auth) + expansions.update(base_expansions) + + display_name = get_display_name(base_name, host, python=python) + variant = create_variant( + [".replica_set", ".7.0"], + display_name, + python=python, + host=host, + expansions=expansions, + batchtime=batchtime, + ) + variants.append(variant) + + return variants + + ################## # Generate Config ################## -project = EvgProject(tasks=None, buildvariants=create_ocsp_variants()) -print(ShrubService.generate_yaml(project)) # noqa: T201 +variants = create_pyopenssl_variants() +# print(len(variants)) +generate_yaml(variants=variants) diff --git a/doc/changelog.rst b/doc/changelog.rst index e7b160b176..4c1955d19d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -14,6 +14,15 @@ PyMongo 4.11 brings a number of changes including: - Dropped support for MongoDB 3.6. - Added support for free-threaded Python with the GIL disabled. For more information see: `Free-threaded CPython `_. +- :attr:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.address` and + :attr:`~pymongo.mongo_client.MongoClient.address` now correctly block when called on unconnected clients + until either connection succeeds or a server selection timeout error is raised. +- Added :func:`repr` support to :class:`pymongo.operations.IndexModel`. +- Added :func:`repr` support to :class:`pymongo.operations.SearchIndexModel`. +- Added ``sort`` parameter to + :meth:`~pymongo.collection.Collection.update_one`, :meth:`~pymongo.collection.Collection.replace_one`, + :class:`~pymongo.operations.UpdateOne`, and + :class:`~pymongo.operations.UpdateMany`, Issues Resolved ............... diff --git a/pymongo/asynchronous/bulk.py b/pymongo/asynchronous/bulk.py index 9d33a990ed..e6cfe5b36e 100644 --- a/pymongo/asynchronous/bulk.py +++ b/pymongo/asynchronous/bulk.py @@ -109,6 +109,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = True self.retrying = False self.started_retryable_write = False @@ -144,6 +145,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -159,6 +161,9 @@ def add_update( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -171,6 +176,7 @@ def add_replace( upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -181,6 +187,9 @@ def add_replace( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append((_UPDATE, cmd)) def add_delete( @@ -699,6 +708,10 @@ async def execute_no_results( raise ConfigurationError( "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." ) + if unack and self.uses_sort and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) # Cannot have both unacknowledged writes and bypass document validation. if self.bypass_doc_val: raise OperationFailure( diff --git a/pymongo/asynchronous/client_bulk.py b/pymongo/asynchronous/client_bulk.py index dc800c9549..96571c21eb 100644 --- a/pymongo/asynchronous/client_bulk.py +++ b/pymongo/asynchronous/client_bulk.py @@ -118,6 +118,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = self.client.options.retry_writes self.retrying = False @@ -148,6 +149,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -169,6 +171,9 @@ def add_update( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -184,6 +189,7 @@ def add_replace( upsert: Optional[bool] = None, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -202,6 +208,9 @@ def add_replace( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append(("replace", cmd)) self.namespaces.append(namespace) self.total_ops += 1 diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 4ddcbab4d2..9b73423627 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -993,6 +993,7 @@ async def _update( session: Optional[AsyncClientSession] = None, retryable_write: bool = False, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1024,6 +1025,14 @@ async def _update( if not isinstance(hint, str): hint = helpers_shared._index_document(hint) update_doc["hint"] = hint + if sort is not None: + if not acknowledged and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) + common.validate_is_mapping("sort", sort) + update_doc["sort"] = sort + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} if let is not None: common.validate_is_mapping("let", let) @@ -1079,6 +1088,7 @@ async def _update_retryable( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1102,6 +1112,7 @@ async def _update( session=session, retryable_write=retryable_write, let=let, + sort=sort, comment=comment, ) @@ -1122,6 +1133,7 @@ async def replace_one( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Replace a single document matching the filter. @@ -1176,8 +1188,13 @@ async def replace_one( aggregate expression context (e.g. "$$var"). :param comment: A user-provided comment to attach to this command. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.11 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1209,6 +1226,7 @@ async def replace_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, @@ -1225,6 +1243,7 @@ async def update_one( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Update a single document matching the filter. @@ -1283,11 +1302,16 @@ async def update_one( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.11 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1322,6 +1346,7 @@ async def update_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index d14a21f41d..aa16e85a07 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -205,7 +205,7 @@ async def _async_sendall_ssl( total_sent += sent async def _async_receive_ssl( - conn: _sslConn, length: int, dummy: AbstractEventLoop + conn: _sslConn, length: int, dummy: AbstractEventLoop, once: Optional[bool] = False ) -> memoryview: mv = memoryview(bytearray(length)) total_read = 0 @@ -217,6 +217,9 @@ async def _async_receive_ssl( read = conn.recv_into(mv[total_read:]) if read == 0: raise OSError("connection closed") + # KMS responses update their expected size after the first batch, stop reading after one loop + if once: + return mv[:read] except BLOCKING_IO_ERRORS: await asyncio.sleep(backoff) read = 0 diff --git a/pymongo/operations.py b/pymongo/operations.py index d2e1feba69..8905048c4e 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -325,6 +325,7 @@ class ReplaceOne(Generic[_DocumentType]): "_collation", "_hint", "_namespace", + "_sort", ) def __init__( @@ -335,6 +336,7 @@ def __init__( collation: Optional[_CollationIn] = None, hint: Optional[_IndexKeyHint] = None, namespace: Optional[str] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a ReplaceOne instance. @@ -353,8 +355,12 @@ def __init__( :meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :param namespace: (optional) The namespace in which to replace a document. + .. versionchanged:: 4.10 + Added ``sort`` option. .. versionchanged:: 4.9 Added the `namespace` option to support `MongoClient.bulk_write`. .. versionchanged:: 3.11 @@ -371,6 +377,7 @@ def __init__( else: self._hint = hint + self._sort = sort self._filter = filter self._doc = replacement self._upsert = upsert @@ -385,6 +392,7 @@ def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: self._upsert, collation=validate_collation_or_none(self._collation), hint=self._hint, + sort=self._sort, ) def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: @@ -400,6 +408,7 @@ def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: self._upsert, collation=validate_collation_or_none(self._collation), hint=self._hint, + sort=self._sort, ) def __eq__(self, other: Any) -> bool: @@ -411,13 +420,15 @@ def __eq__(self, other: Any) -> bool: other._collation, other._hint, other._namespace, + other._sort, ) == ( self._filter, self._doc, self._upsert, self._collation, - other._hint, + self._hint, self._namespace, + self._sort, ) return NotImplemented @@ -426,7 +437,7 @@ def __ne__(self, other: Any) -> bool: def __repr__(self) -> str: if self._namespace: - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -434,14 +445,16 @@ def __repr__(self) -> str: self._collation, self._hint, self._namespace, + self._sort, ) - return "{}({!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, self._upsert, self._collation, self._hint, + self._sort, ) @@ -456,6 +469,7 @@ class _UpdateOp: "_array_filters", "_hint", "_namespace", + "_sort", ) def __init__( @@ -467,6 +481,7 @@ def __init__( array_filters: Optional[list[Mapping[str, Any]]], hint: Optional[_IndexKeyHint], namespace: Optional[str], + sort: Optional[Mapping[str, Any]], ): if filter is not None: validate_is_mapping("filter", filter) @@ -478,13 +493,13 @@ def __init__( self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint) else: self._hint = hint - self._filter = filter self._doc = doc self._upsert = upsert self._collation = collation self._array_filters = array_filters self._namespace = namespace + self._sort = sort def __eq__(self, other: object) -> bool: if isinstance(other, type(self)): @@ -496,6 +511,7 @@ def __eq__(self, other: object) -> bool: other._array_filters, other._hint, other._namespace, + other._sort, ) == ( self._filter, self._doc, @@ -504,6 +520,7 @@ def __eq__(self, other: object) -> bool: self._array_filters, self._hint, self._namespace, + self._sort, ) return NotImplemented @@ -512,7 +529,7 @@ def __ne__(self, other: Any) -> bool: def __repr__(self) -> str: if self._namespace: - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -521,8 +538,9 @@ def __repr__(self) -> str: self._array_filters, self._hint, self._namespace, + self._sort, ) - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -530,6 +548,7 @@ def __repr__(self) -> str: self._collation, self._array_filters, self._hint, + self._sort, ) @@ -547,6 +566,7 @@ def __init__( array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, namespace: Optional[str] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Represents an update_one operation. @@ -567,8 +587,12 @@ def __init__( :meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. - :param namespace: (optional) The namespace in which to update a document. + :param namespace: The namespace in which to update a document. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + .. versionchanged:: 4.10 + Added ``sort`` option. .. versionchanged:: 4.9 Added the `namespace` option to support `MongoClient.bulk_write`. .. versionchanged:: 3.11 @@ -580,7 +604,7 @@ def __init__( .. versionchanged:: 3.5 Added the `collation` option. """ - super().__init__(filter, update, upsert, collation, array_filters, hint, namespace) + super().__init__(filter, update, upsert, collation, array_filters, hint, namespace, sort) def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: """Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`.""" @@ -592,6 +616,7 @@ def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: collation=validate_collation_or_none(self._collation), array_filters=self._array_filters, hint=self._hint, + sort=self._sort, ) def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: @@ -609,6 +634,7 @@ def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: collation=validate_collation_or_none(self._collation), array_filters=self._array_filters, hint=self._hint, + sort=self._sort, ) @@ -659,7 +685,7 @@ def __init__( .. versionchanged:: 3.5 Added the `collation` option. """ - super().__init__(filter, update, upsert, collation, array_filters, hint, namespace) + super().__init__(filter, update, upsert, collation, array_filters, hint, namespace, None) def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: """Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`.""" @@ -773,6 +799,13 @@ def document(self) -> dict[str, Any]: """ return self.__document + def __repr__(self) -> str: + return "{}({}{})".format( + self.__class__.__name__, + self.document["key"], + "".join([f", {key}={value!r}" for key, value in self.document.items() if key != "key"]), + ) + class SearchIndexModel: """Represents a search index to create.""" @@ -812,3 +845,9 @@ def __init__( def document(self) -> Mapping[str, Any]: """The document for this index.""" return self.__document + + def __repr__(self) -> str: + return "{}({})".format( + self.__class__.__name__, + ", ".join([f"{key}={value!r}" for key, value in self.document.items()]), + ) diff --git a/pymongo/synchronous/bulk.py b/pymongo/synchronous/bulk.py index c658157ea1..7fb29a977f 100644 --- a/pymongo/synchronous/bulk.py +++ b/pymongo/synchronous/bulk.py @@ -109,6 +109,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = True self.retrying = False self.started_retryable_write = False @@ -144,6 +145,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -159,6 +161,9 @@ def add_update( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -171,6 +176,7 @@ def add_replace( upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -181,6 +187,9 @@ def add_replace( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append((_UPDATE, cmd)) def add_delete( @@ -697,6 +706,10 @@ def execute_no_results( raise ConfigurationError( "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." ) + if unack and self.uses_sort and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) # Cannot have both unacknowledged writes and bypass document validation. if self.bypass_doc_val: raise OperationFailure( diff --git a/pymongo/synchronous/client_bulk.py b/pymongo/synchronous/client_bulk.py index f41f0203f2..2c38b1d76c 100644 --- a/pymongo/synchronous/client_bulk.py +++ b/pymongo/synchronous/client_bulk.py @@ -118,6 +118,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = self.client.options.retry_writes self.retrying = False @@ -148,6 +149,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -169,6 +171,9 @@ def add_update( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -184,6 +189,7 @@ def add_replace( upsert: Optional[bool] = None, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -202,6 +208,9 @@ def add_replace( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append(("replace", cmd)) self.namespaces.append(namespace) self.total_ops += 1 diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 6fd2ac82dd..6edfddc9a9 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -992,6 +992,7 @@ def _update( session: Optional[ClientSession] = None, retryable_write: bool = False, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1023,6 +1024,14 @@ def _update( if not isinstance(hint, str): hint = helpers_shared._index_document(hint) update_doc["hint"] = hint + if sort is not None: + if not acknowledged and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) + common.validate_is_mapping("sort", sort) + update_doc["sort"] = sort + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} if let is not None: common.validate_is_mapping("let", let) @@ -1078,6 +1087,7 @@ def _update_retryable( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1101,6 +1111,7 @@ def _update( session=session, retryable_write=retryable_write, let=let, + sort=sort, comment=comment, ) @@ -1121,6 +1132,7 @@ def replace_one( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Replace a single document matching the filter. @@ -1175,8 +1187,13 @@ def replace_one( aggregate expression context (e.g. "$$var"). :param comment: A user-provided comment to attach to this command. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.11 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1208,6 +1225,7 @@ def replace_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, @@ -1224,6 +1242,7 @@ def update_one( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Update a single document matching the filter. @@ -1282,11 +1301,16 @@ def update_one( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.11 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1321,6 +1345,7 @@ def update_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, diff --git a/pyproject.toml b/pyproject.toml index b4f59f67d5..9a29a777fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ filterwarnings = [ markers = [ "auth_aws: tests that rely on pymongo-auth-aws", "auth_oidc: tests that rely on oidc auth", + "auth: tests that rely on authentication", "ocsp: tests that rely on ocsp", "atlas: tests that rely on atlas", "data_lake: tests that rely on atlas data lake", diff --git a/requirements/typing.txt b/requirements/typing.txt index 06c33c6db6..2c23212da7 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -1,5 +1,5 @@ mypy==1.11.2 -pyright==1.1.383 +pyright==1.1.384 typing_extensions -r ./encryption.txt -r ./ocsp.txt diff --git a/test/asynchronous/test_auth.py b/test/asynchronous/test_auth.py index fbaca41f09..9262714374 100644 --- a/test/asynchronous/test_auth.py +++ b/test/asynchronous/test_auth.py @@ -32,6 +32,8 @@ ) from test.utils import AllowListEventListener, delay, ignore_deprecations +import pytest + from pymongo import AsyncMongoClient, monitoring from pymongo.asynchronous.auth import HAVE_KERBEROS from pymongo.auth_shared import _build_credentials_tuple @@ -42,6 +44,8 @@ _IS_SYNC = False +pytestmark = pytest.mark.auth + # YOU MUST RUN KINIT BEFORE RUNNING GSSAPI TESTS ON UNIX. GSSAPI_HOST = os.environ.get("GSSAPI_HOST") GSSAPI_PORT = int(os.environ.get("GSSAPI_PORT", "27017")) diff --git a/test/asynchronous/test_bulk.py b/test/asynchronous/test_bulk.py index e01dd53d7e..7191a412c1 100644 --- a/test/asynchronous/test_bulk.py +++ b/test/asynchronous/test_bulk.py @@ -961,6 +961,9 @@ async def cause_wtimeout(self, requests, ordered): @async_client_context.require_replica_set @async_client_context.require_secondaries_count(1) async def test_write_concern_failure_ordered(self): + self.skipTest("Skipping until PYTHON-4865 is resolved.") + details = None + # Ensure we don't raise on wnote. coll_ww = self.coll.with_options(write_concern=WriteConcern(w=self.w)) result = await coll_ww.bulk_write([DeleteOne({"something": "that does no exist"})]) @@ -1041,6 +1044,9 @@ async def test_write_concern_failure_ordered(self): @async_client_context.require_replica_set @async_client_context.require_secondaries_count(1) async def test_write_concern_failure_unordered(self): + self.skipTest("Skipping until PYTHON-4865 is resolved.") + details = None + # Ensure we don't raise on wnote. coll_ww = self.coll.with_options(write_concern=WriteConcern(w=self.w)) result = await coll_ww.bulk_write( diff --git a/test/asynchronous/test_change_stream.py b/test/asynchronous/test_change_stream.py index db8a74f55a..873631bbe5 100644 --- a/test/asynchronous/test_change_stream.py +++ b/test/asynchronous/test_change_stream.py @@ -39,6 +39,7 @@ from test.utils import ( AllowListEventListener, EventListener, + OvertCommandListener, async_wait_until, ) @@ -179,7 +180,7 @@ async def _wait_until(): @no_type_check async def test_try_next_runs_one_getmore(self): - listener = EventListener() + listener = OvertCommandListener() client = await self.async_rs_or_single_client(event_listeners=[listener]) # Connect to the cluster. await client.admin.command("ping") @@ -237,7 +238,7 @@ async def _wait_until(): @no_type_check async def test_batch_size_is_honored(self): - listener = EventListener() + listener = OvertCommandListener() client = await self.async_rs_or_single_client(event_listeners=[listener]) # Connect to the cluster. await client.admin.command("ping") diff --git a/test/asynchronous/test_collation.py b/test/asynchronous/test_collation.py index abbca1aff9..d7fd85b168 100644 --- a/test/asynchronous/test_collation.py +++ b/test/asynchronous/test_collation.py @@ -18,7 +18,7 @@ import functools import warnings from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest -from test.utils import EventListener +from test.utils import EventListener, OvertCommandListener from typing import Any from pymongo.asynchronous.helpers import anext @@ -100,13 +100,12 @@ class TestCollation(AsyncIntegrationTest): @async_client_context.require_connection async def asyncSetUp(self) -> None: await super().asyncSetUp() - self.listener = EventListener() + self.listener = OvertCommandListener() self.client = await self.async_rs_or_single_client(event_listeners=[self.listener]) self.db = self.client.pymongo_test self.collation = Collation("en_US") self.warn_context = warnings.catch_warnings() self.warn_context.__enter__() - warnings.simplefilter("ignore", DeprecationWarning) async def asyncTearDown(self) -> None: self.warn_context.__exit__() diff --git a/test/asynchronous/test_collection.py b/test/asynchronous/test_collection.py index a2ed4de388..528919f63c 100644 --- a/test/asynchronous/test_collection.py +++ b/test/asynchronous/test_collection.py @@ -36,6 +36,7 @@ from test.utils import ( IMPOSSIBLE_WRITE_CONCERN, EventListener, + OvertCommandListener, async_get_pool, async_is_mongos, async_wait_until, @@ -2101,7 +2102,7 @@ async def test_find_one_and(self): self.assertEqual(4, (await c.find_one_and_update({}, {"$inc": {"i": 1}}, sort=sort))["j"]) async def test_find_one_and_write_concern(self): - listener = EventListener() + listener = OvertCommandListener() db = (await self.async_single_client(event_listeners=[listener]))[self.db.name] # non-default WriteConcern. c_w0 = db.get_collection("test", write_concern=WriteConcern(w=0)) diff --git a/test/asynchronous/test_collection_management.py b/test/asynchronous/test_collection_management.py new file mode 100644 index 0000000000..c0edf91581 --- /dev/null +++ b/test/asynchronous/test_collection_management.py @@ -0,0 +1,41 @@ +# Copyright 2021-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 the collection management unified spec tests.""" +from __future__ import annotations + +import os +import pathlib +import sys + +sys.path[0:0] = [""] + +from test import unittest +from test.asynchronous.unified_format import generate_test_classes + +_IS_SYNC = False + +# Location of JSON test specifications. +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "collection_management") +else: + _TEST_PATH = os.path.join( + pathlib.Path(__file__).resolve().parent.parent, "collection_management" + ) + +# Generate unified tests. +globals().update(generate_test_classes(_TEST_PATH, module=__name__)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_create_entities.py b/test/asynchronous/test_create_entities.py new file mode 100644 index 0000000000..cb2ec63f4c --- /dev/null +++ b/test/asynchronous/test_create_entities.py @@ -0,0 +1,128 @@ +# Copyright 2021-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. +from __future__ import annotations + +import sys +import unittest + +sys.path[0:0] = [""] + +from test.asynchronous import AsyncIntegrationTest +from test.asynchronous.unified_format import UnifiedSpecTestMixinV1 + +_IS_SYNC = False + + +class TestCreateEntities(AsyncIntegrationTest): + async def test_store_events_as_entities(self): + self.scenario_runner = UnifiedSpecTestMixinV1() + spec = { + "description": "blank", + "schemaVersion": "1.2", + "createEntities": [ + { + "client": { + "id": "client0", + "storeEventsAsEntities": [ + { + "id": "events1", + "events": [ + "PoolCreatedEvent", + ], + } + ], + } + }, + ], + "tests": [{"description": "foo", "operations": []}], + } + self.scenario_runner.TEST_SPEC = spec + await self.scenario_runner.asyncSetUp() + await self.scenario_runner.run_scenario(spec["tests"][0]) + await self.scenario_runner.entity_map["client0"].close() + final_entity_map = self.scenario_runner.entity_map + self.assertIn("events1", final_entity_map) + self.assertGreater(len(final_entity_map["events1"]), 0) + for event in final_entity_map["events1"]: + self.assertIn("PoolCreatedEvent", event["name"]) + + async def test_store_all_others_as_entities(self): + self.scenario_runner = UnifiedSpecTestMixinV1() + spec = { + "description": "Find", + "schemaVersion": "1.2", + "createEntities": [ + { + "client": { + "id": "client0", + "uriOptions": {"retryReads": True}, + } + }, + {"database": {"id": "database0", "client": "client0", "databaseName": "dat"}}, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "dat", + } + }, + ], + "tests": [ + { + "description": "test loops", + "operations": [ + { + "name": "loop", + "object": "testRunner", + "arguments": { + "storeIterationsAsEntity": "iterations", + "storeSuccessesAsEntity": "successes", + "storeFailuresAsEntity": "failures", + "storeErrorsAsEntity": "errors", + "numIterations": 5, + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": {"document": {"_id": 1, "x": 44}}, + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": {"document": {"_id": 2, "x": 44}}, + }, + ], + }, + } + ], + } + ], + } + + await self.client.dat.dat.delete_many({}) + self.scenario_runner.TEST_SPEC = spec + await self.scenario_runner.asyncSetUp() + await self.scenario_runner.run_scenario(spec["tests"][0]) + await self.scenario_runner.entity_map["client0"].close() + entity_map = self.scenario_runner.entity_map + self.assertEqual(len(entity_map["errors"]), 4) + for error in entity_map["errors"]: + self.assertEqual(error["type"], "DuplicateKeyError") + self.assertEqual(entity_map["failures"], []) + self.assertEqual(entity_map["successes"], 2) + self.assertEqual(entity_map["iterations"], 5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/asynchronous/test_cursor.py b/test/asynchronous/test_cursor.py index 09955ca66f..d216479451 100644 --- a/test/asynchronous/test_cursor.py +++ b/test/asynchronous/test_cursor.py @@ -1601,7 +1601,7 @@ async def test_read_concern(self): await anext(c.find_raw_batches()) async def test_monitoring(self): - listener = EventListener() + listener = OvertCommandListener() client = await self.async_rs_or_single_client(event_listeners=[listener]) c = client.pymongo_test.test await c.drop() @@ -1764,7 +1764,7 @@ async def test_collation(self): await anext(await self.db.test.aggregate_raw_batches([], collation=Collation("en_US"))) async def test_monitoring(self): - listener = EventListener() + listener = OvertCommandListener() client = await self.async_rs_or_single_client(event_listeners=[listener]) c = client.pymongo_test.test await c.drop() diff --git a/test/asynchronous/test_grid_file.py b/test/asynchronous/test_grid_file.py index 14446106e0..affdacde91 100644 --- a/test/asynchronous/test_grid_file.py +++ b/test/asynchronous/test_grid_file.py @@ -33,7 +33,7 @@ sys.path[0:0] = [""] -from test.utils import EventListener +from test.utils import OvertCommandListener from bson.objectid import ObjectId from gridfs.asynchronous.grid_file import ( @@ -811,7 +811,7 @@ async def test_survive_cursor_not_found(self): # Use 102 batches to cause a single getMore. chunk_size = 1024 data = b"d" * (102 * chunk_size) - listener = EventListener() + listener = OvertCommandListener() client = await self.async_rs_or_single_client(event_listeners=[listener]) db = client.pymongo_test async with AsyncGridIn(db.fs, chunk_size=chunk_size) as infile: diff --git a/test/asynchronous/test_monitoring.py b/test/asynchronous/test_monitoring.py index a5f991b2f0..eaad60beac 100644 --- a/test/asynchronous/test_monitoring.py +++ b/test/asynchronous/test_monitoring.py @@ -31,6 +31,7 @@ ) from test.utils import ( EventListener, + OvertCommandListener, async_wait_until, ) @@ -52,7 +53,7 @@ class AsyncTestCommandMonitoring(AsyncIntegrationTest): @classmethod def setUpClass(cls) -> None: - cls.listener = EventListener() + cls.listener = OvertCommandListener() @async_client_context.require_connection async def asyncSetUp(self) -> None: @@ -1094,11 +1095,13 @@ async def test_first_batch_helper(self): @async_client_context.require_version_max(6, 1, 99) async def test_sensitive_commands(self): - listeners = self.client._event_listeners + listener = EventListener() + client = await self.async_rs_or_single_client(event_listeners=[listener]) + listeners = client._event_listeners - self.listener.reset() + listener.reset() cmd = SON([("getnonce", 1)]) - listeners.publish_command_start(cmd, "pymongo_test", 12345, await self.client.address, None) # type: ignore[arg-type] + listeners.publish_command_start(cmd, "pymongo_test", 12345, await client.address, None) # type: ignore[arg-type] delta = datetime.timedelta(milliseconds=100) listeners.publish_command_success( delta, @@ -1109,15 +1112,15 @@ async def test_sensitive_commands(self): None, database_name="pymongo_test", ) - started = self.listener.started_events[0] - succeeded = self.listener.succeeded_events[0] - self.assertEqual(0, len(self.listener.failed_events)) + started = listener.started_events[0] + succeeded = listener.succeeded_events[0] + self.assertEqual(0, len(listener.failed_events)) self.assertIsInstance(started, monitoring.CommandStartedEvent) self.assertEqual({}, started.command) self.assertEqual("pymongo_test", started.database_name) self.assertEqual("getnonce", started.command_name) self.assertIsInstance(started.request_id, int) - self.assertEqual(await self.client.address, started.connection_id) + self.assertEqual(await client.address, started.connection_id) self.assertIsInstance(succeeded, monitoring.CommandSucceededEvent) self.assertEqual(succeeded.duration_micros, 100000) self.assertEqual(started.command_name, succeeded.command_name) @@ -1132,7 +1135,7 @@ class AsyncTestGlobalListener(AsyncIntegrationTest): @classmethod def setUpClass(cls) -> None: - cls.listener = EventListener() + cls.listener = OvertCommandListener() # We plan to call register(), which internally modifies _LISTENERS. cls.saved_listeners = copy.deepcopy(monitoring._LISTENERS) monitoring.register(cls.listener) @@ -1140,17 +1143,11 @@ def setUpClass(cls) -> None: @async_client_context.require_connection async def asyncSetUp(self): await super().asyncSetUp() - self.listener = EventListener() - # We plan to call register(), which internally modifies _LISTENERS. - self.saved_listeners = copy.deepcopy(monitoring._LISTENERS) - monitoring.register(self.listener) + self.listener.reset() self.client = await self.async_single_client() # Get one (authenticated) socket in the pool. await self.client.pymongo_test.command("ping") - async def asyncTearDown(self) -> None: - self.listener.reset() - @classmethod def tearDownClass(cls): monitoring._LISTENERS = cls.saved_listeners diff --git a/test/asynchronous/test_session.py b/test/asynchronous/test_session.py index e424796ce0..42bc253b56 100644 --- a/test/asynchronous/test_session.py +++ b/test/asynchronous/test_session.py @@ -36,6 +36,7 @@ from test.utils import ( EventListener, ExceptionCatchingThread, + OvertCommandListener, async_wait_until, ) @@ -191,7 +192,7 @@ def test_implicit_sessions_checkout(self): lsid_set = set() failures = 0 for _ in range(5): - listener = EventListener() + listener = OvertCommandListener() client = self.async_rs_or_single_client(event_listeners=[listener], maxPoolSize=1) cursor = client.db.test.find({}) ops: List[Tuple[Callable, List[Any]]] = [ diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index f25e96e04d..11b124a124 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -772,7 +772,7 @@ async def _databaseOperation_listCollections(self, target, *args, **kwargs): if "batch_size" in kwargs: kwargs["cursor"] = {"batchSize": kwargs.pop("batch_size")} cursor = await target.list_collections(*args, **kwargs) - return list(cursor) + return await cursor.to_list() async def _databaseOperation_createCollection(self, target, *args, **kwargs): # PYTHON-1936 Ignore the listCollections event from create_collection. diff --git a/test/auth_oidc/test_auth_oidc.py b/test/auth_oidc/test_auth_oidc.py index 6d31f3db4e..6526391daf 100644 --- a/test/auth_oidc/test_auth_oidc.py +++ b/test/auth_oidc/test_auth_oidc.py @@ -31,7 +31,7 @@ sys.path[0:0] = [""] from test.unified_format import generate_test_classes -from test.utils import EventListener +from test.utils import EventListener, OvertCommandListener from bson import SON from pymongo import MongoClient @@ -348,7 +348,7 @@ def test_4_1_reauthenticate_succeeds(self): # Create a default OIDC client and add an event listener. # The following assumes that the driver does not emit saslStart or saslContinue events. # If the driver does emit those events, ignore/filter them for the purposes of this test. - listener = EventListener() + listener = OvertCommandListener() client = self.create_client(event_listeners=[listener]) # Perform a find operation that succeeds. @@ -1021,7 +1021,7 @@ def fetch(self, _): def test_4_4_speculative_authentication_should_be_ignored_on_reauthentication(self): # Create an OIDC configured client that can listen for `SaslStart` commands. - listener = EventListener() + listener = OvertCommandListener() client = self.create_client(event_listeners=[listener]) # Preload the *Client Cache* with a valid access token to enforce Speculative Authentication. diff --git a/test/crud/unified/aggregate-write-readPreference.json b/test/crud/unified/aggregate-write-readPreference.json index bc887e83cb..c1fa3b4574 100644 --- a/test/crud/unified/aggregate-write-readPreference.json +++ b/test/crud/unified/aggregate-write-readPreference.json @@ -78,11 +78,6 @@ "x": 33 } ] - }, - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [] } ], "tests": [ @@ -159,22 +154,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -250,22 +229,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -344,22 +307,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -438,22 +385,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] } ] diff --git a/test/crud/unified/bulkWrite-replaceOne-sort.json b/test/crud/unified/bulkWrite-replaceOne-sort.json new file mode 100644 index 0000000000..c0bd383514 --- /dev/null +++ b/test/crud/unified/bulkWrite-replaceOne-sort.json @@ -0,0 +1,239 @@ +{ + "description": "BulkWrite replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite replaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/bulkWrite-updateOne-sort.json b/test/crud/unified/bulkWrite-updateOne-sort.json new file mode 100644 index 0000000000..f78bd3bf3e --- /dev/null +++ b/test/crud/unified/bulkWrite-updateOne-sort.json @@ -0,0 +1,255 @@ +{ + "description": "BulkWrite updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite updateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/client-bulkWrite-partialResults.json b/test/crud/unified/client-bulkWrite-partialResults.json new file mode 100644 index 0000000000..b35e94a2ea --- /dev/null +++ b/test/crud/unified/client-bulkWrite-partialResults.json @@ -0,0 +1,540 @@ +{ + "description": "client bulkWrite partial results", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "newDocument": { + "_id": 2, + "x": 22 + } + }, + "tests": [ + { + "description": "partialResult is unset when first operation fails during an ordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": true, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is unset when first operation fails during an ordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": true, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an ordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": true, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an ordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": true, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + }, + { + "description": "partialResult is unset when all operations fail during an unordered bulk write", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": false, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "1": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": false, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an unordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + } + ] +} diff --git a/test/crud/unified/client-bulkWrite-replaceOne-sort.json b/test/crud/unified/client-bulkWrite-replaceOne-sort.json new file mode 100644 index 0000000000..53218c1f48 --- /dev/null +++ b/test/crud/unified/client-bulkWrite-replaceOne-sort.json @@ -0,0 +1,162 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite replaceOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/client-bulkWrite-updateOne-sort.json b/test/crud/unified/client-bulkWrite-updateOne-sort.json new file mode 100644 index 0000000000..4a07b8b97c --- /dev/null +++ b/test/crud/unified/client-bulkWrite-updateOne-sort.json @@ -0,0 +1,166 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite updateOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/db-aggregate-write-readPreference.json b/test/crud/unified/db-aggregate-write-readPreference.json index 2a81282de8..b6460f001f 100644 --- a/test/crud/unified/db-aggregate-write-readPreference.json +++ b/test/crud/unified/db-aggregate-write-readPreference.json @@ -52,13 +52,6 @@ } } ], - "initialData": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [] - } - ], "tests": [ { "description": "Database-level aggregate with $out includes read preference for 5.0+ server", @@ -141,17 +134,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -235,17 +217,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -332,17 +303,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -429,17 +389,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] } ] diff --git a/test/crud/unified/replaceOne-sort.json b/test/crud/unified/replaceOne-sort.json new file mode 100644 index 0000000000..cf2271dda5 --- /dev/null +++ b/test/crud/unified/replaceOne-sort.json @@ -0,0 +1,232 @@ +{ + "description": "replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "ReplaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/updateOne-sort.json b/test/crud/unified/updateOne-sort.json new file mode 100644 index 0000000000..8fe4f50b94 --- /dev/null +++ b/test/crud/unified/updateOne-sort.json @@ -0,0 +1,240 @@ +{ + "description": "updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "UpdateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + }, + { + "description": "updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/test_auth.py b/test/test_auth.py index b311d330bc..310006afff 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -32,6 +32,8 @@ ) from test.utils import AllowListEventListener, delay, ignore_deprecations +import pytest + from pymongo import MongoClient, monitoring from pymongo.auth_shared import _build_credentials_tuple from pymongo.errors import OperationFailure @@ -42,6 +44,8 @@ _IS_SYNC = True +pytestmark = pytest.mark.auth + # YOU MUST RUN KINIT BEFORE RUNNING GSSAPI TESTS ON UNIX. GSSAPI_HOST = os.environ.get("GSSAPI_HOST") GSSAPI_PORT = int(os.environ.get("GSSAPI_PORT", "27017")) diff --git a/test/test_bulk.py b/test/test_bulk.py index ad22c1ce9a..6d29ff510a 100644 --- a/test/test_bulk.py +++ b/test/test_bulk.py @@ -959,6 +959,9 @@ def cause_wtimeout(self, requests, ordered): @client_context.require_replica_set @client_context.require_secondaries_count(1) def test_write_concern_failure_ordered(self): + self.skipTest("Skipping until PYTHON-4865 is resolved.") + details = None + # Ensure we don't raise on wnote. coll_ww = self.coll.with_options(write_concern=WriteConcern(w=self.w)) result = coll_ww.bulk_write([DeleteOne({"something": "that does no exist"})]) @@ -1039,6 +1042,9 @@ def test_write_concern_failure_ordered(self): @client_context.require_replica_set @client_context.require_secondaries_count(1) def test_write_concern_failure_unordered(self): + self.skipTest("Skipping until PYTHON-4865 is resolved.") + details = None + # Ensure we don't raise on wnote. coll_ww = self.coll.with_options(write_concern=WriteConcern(w=self.w)) result = coll_ww.bulk_write([DeleteOne({"something": "that does no exist"})], ordered=False) diff --git a/test/test_change_stream.py b/test/test_change_stream.py index 0742384184..4ed21f55cf 100644 --- a/test/test_change_stream.py +++ b/test/test_change_stream.py @@ -39,6 +39,7 @@ from test.utils import ( AllowListEventListener, EventListener, + OvertCommandListener, wait_until, ) @@ -177,7 +178,7 @@ def _wait_until(): @no_type_check def test_try_next_runs_one_getmore(self): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) # Connect to the cluster. client.admin.command("ping") @@ -235,7 +236,7 @@ def _wait_until(): @no_type_check def test_batch_size_is_honored(self): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) # Connect to the cluster. client.admin.command("ping") diff --git a/test/test_collation.py b/test/test_collation.py index 6d4e958a1f..06436f0638 100644 --- a/test/test_collation.py +++ b/test/test_collation.py @@ -18,7 +18,7 @@ import functools import warnings from test import IntegrationTest, client_context, unittest -from test.utils import EventListener +from test.utils import EventListener, OvertCommandListener from typing import Any from pymongo.collation import ( @@ -100,13 +100,12 @@ class TestCollation(IntegrationTest): @client_context.require_connection def setUp(self) -> None: super().setUp() - self.listener = EventListener() + self.listener = OvertCommandListener() self.client = self.rs_or_single_client(event_listeners=[self.listener]) self.db = self.client.pymongo_test self.collation = Collation("en_US") self.warn_context = warnings.catch_warnings() self.warn_context.__enter__() - warnings.simplefilter("ignore", DeprecationWarning) def tearDown(self) -> None: self.warn_context.__exit__() diff --git a/test/test_collection.py b/test/test_collection.py index 9364d34e34..af524bba47 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -36,6 +36,7 @@ from test.utils import ( IMPOSSIBLE_WRITE_CONCERN, EventListener, + OvertCommandListener, get_pool, is_mongos, wait_until, @@ -2079,7 +2080,7 @@ def test_find_one_and(self): self.assertEqual(4, (c.find_one_and_update({}, {"$inc": {"i": 1}}, sort=sort))["j"]) def test_find_one_and_write_concern(self): - listener = EventListener() + listener = OvertCommandListener() db = (self.single_client(event_listeners=[listener]))[self.db.name] # non-default WriteConcern. c_w0 = db.get_collection("test", write_concern=WriteConcern(w=0)) diff --git a/test/test_collection_management.py b/test/test_collection_management.py index 0eacde1302..063c20df8f 100644 --- a/test/test_collection_management.py +++ b/test/test_collection_management.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -23,11 +24,18 @@ from test import unittest from test.unified_format import generate_test_classes +_IS_SYNC = True + # Location of JSON test specifications. -TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "collection_management") +if _IS_SYNC: + _TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "collection_management") +else: + _TEST_PATH = os.path.join( + pathlib.Path(__file__).resolve().parent.parent, "collection_management" + ) # Generate unified tests. -globals().update(generate_test_classes(TEST_PATH, module=__name__)) +globals().update(generate_test_classes(_TEST_PATH, module=__name__)) if __name__ == "__main__": unittest.main() diff --git a/test/test_create_entities.py b/test/test_create_entities.py index b7965d4a1d..ad75fe5702 100644 --- a/test/test_create_entities.py +++ b/test/test_create_entities.py @@ -21,6 +21,8 @@ from test import IntegrationTest from test.unified_format import UnifiedSpecTestMixinV1 +_IS_SYNC = True + class TestCreateEntities(IntegrationTest): def test_store_events_as_entities(self): diff --git a/test/test_cursor.py b/test/test_cursor.py index e687abcfbf..bcc7ed75f1 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -1590,7 +1590,7 @@ def test_read_concern(self): next(c.find_raw_batches()) def test_monitoring(self): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) c = client.pymongo_test.test c.drop() @@ -1753,7 +1753,7 @@ def test_collation(self): next(self.db.test.aggregate_raw_batches([], collation=Collation("en_US"))) def test_monitoring(self): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) c = client.pymongo_test.test c.drop() diff --git a/test/test_grid_file.py b/test/test_grid_file.py index 0a5b1ad40a..6534bc11bf 100644 --- a/test/test_grid_file.py +++ b/test/test_grid_file.py @@ -33,7 +33,7 @@ sys.path[0:0] = [""] -from test.utils import EventListener +from test.utils import OvertCommandListener from bson.objectid import ObjectId from gridfs.errors import NoFile @@ -809,7 +809,7 @@ def test_survive_cursor_not_found(self): # Use 102 batches to cause a single getMore. chunk_size = 1024 data = b"d" * (102 * chunk_size) - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) db = client.pymongo_test with GridIn(db.fs, chunk_size=chunk_size) as infile: diff --git a/test/test_index_management.py b/test/test_index_management.py index ec1e363737..6ca726e2e0 100644 --- a/test/test_index_management.py +++ b/test/test_index_management.py @@ -27,7 +27,7 @@ from test import IntegrationTest, PyMongoTestCase, unittest from test.unified_format import generate_test_classes -from test.utils import AllowListEventListener, EventListener +from test.utils import AllowListEventListener, EventListener, OvertCommandListener from pymongo.errors import OperationFailure from pymongo.operations import SearchIndexModel @@ -88,7 +88,7 @@ def setUpClass(cls) -> None: url = os.environ.get("MONGODB_URI") username = os.environ["DB_USER"] password = os.environ["DB_PASSWORD"] - cls.listener = listener = EventListener() + cls.listener = listener = OvertCommandListener() cls.client = cls.unmanaged_simple_client( url, username=username, password=password, event_listeners=[listener] ) diff --git a/test/test_monitoring.py b/test/test_monitoring.py index 31f546fe54..670558c0a0 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -31,6 +31,7 @@ ) from test.utils import ( EventListener, + OvertCommandListener, wait_until, ) @@ -52,7 +53,7 @@ class TestCommandMonitoring(IntegrationTest): @classmethod def setUpClass(cls) -> None: - cls.listener = EventListener() + cls.listener = OvertCommandListener() @client_context.require_connection def setUp(self) -> None: @@ -1092,11 +1093,13 @@ def test_first_batch_helper(self): @client_context.require_version_max(6, 1, 99) def test_sensitive_commands(self): - listeners = self.client._event_listeners + listener = EventListener() + client = self.rs_or_single_client(event_listeners=[listener]) + listeners = client._event_listeners - self.listener.reset() + listener.reset() cmd = SON([("getnonce", 1)]) - listeners.publish_command_start(cmd, "pymongo_test", 12345, self.client.address, None) # type: ignore[arg-type] + listeners.publish_command_start(cmd, "pymongo_test", 12345, client.address, None) # type: ignore[arg-type] delta = datetime.timedelta(milliseconds=100) listeners.publish_command_success( delta, @@ -1107,15 +1110,15 @@ def test_sensitive_commands(self): None, database_name="pymongo_test", ) - started = self.listener.started_events[0] - succeeded = self.listener.succeeded_events[0] - self.assertEqual(0, len(self.listener.failed_events)) + started = listener.started_events[0] + succeeded = listener.succeeded_events[0] + self.assertEqual(0, len(listener.failed_events)) self.assertIsInstance(started, monitoring.CommandStartedEvent) self.assertEqual({}, started.command) self.assertEqual("pymongo_test", started.database_name) self.assertEqual("getnonce", started.command_name) self.assertIsInstance(started.request_id, int) - self.assertEqual(self.client.address, started.connection_id) + self.assertEqual(client.address, started.connection_id) self.assertIsInstance(succeeded, monitoring.CommandSucceededEvent) self.assertEqual(succeeded.duration_micros, 100000) self.assertEqual(started.command_name, succeeded.command_name) @@ -1130,7 +1133,7 @@ class TestGlobalListener(IntegrationTest): @classmethod def setUpClass(cls) -> None: - cls.listener = EventListener() + cls.listener = OvertCommandListener() # We plan to call register(), which internally modifies _LISTENERS. cls.saved_listeners = copy.deepcopy(monitoring._LISTENERS) monitoring.register(cls.listener) @@ -1138,17 +1141,11 @@ def setUpClass(cls) -> None: @client_context.require_connection def setUp(self): super().setUp() - self.listener = EventListener() - # We plan to call register(), which internally modifies _LISTENERS. - self.saved_listeners = copy.deepcopy(monitoring._LISTENERS) - monitoring.register(self.listener) + self.listener.reset() self.client = self.single_client() # Get one (authenticated) socket in the pool. self.client.pymongo_test.command("ping") - def tearDown(self) -> None: - self.listener.reset() - @classmethod def tearDownClass(cls): monitoring._LISTENERS = cls.saved_listeners diff --git a/test/test_operations.py b/test/test_operations.py new file mode 100644 index 0000000000..3ee6677735 --- /dev/null +++ b/test/test_operations.py @@ -0,0 +1,80 @@ +# Copyright 2024-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 the operations module.""" +from __future__ import annotations + +from test import UnitTest, unittest + +from pymongo import ASCENDING, DESCENDING +from pymongo.collation import Collation +from pymongo.errors import OperationFailure +from pymongo.operations import IndexModel, SearchIndexModel + + +class TestOperationsBase(UnitTest): + """Base class for testing operations module.""" + + def assertRepr(self, obj): + new_obj = eval(repr(obj)) + self.assertEqual(type(new_obj), type(obj)) + self.assertEqual(repr(new_obj), repr(obj)) + + +class TestIndexModel(TestOperationsBase): + """Test IndexModel features.""" + + def test_repr(self): + # Based on examples in test_collection.py + self.assertRepr(IndexModel("hello")) + self.assertRepr(IndexModel([("hello", DESCENDING), ("world", ASCENDING)])) + self.assertRepr( + IndexModel([("hello", DESCENDING), ("world", ASCENDING)], name="hello_world") + ) + # Test all the kwargs + self.assertRepr(IndexModel("name", name="name")) + self.assertRepr(IndexModel("unique", unique=False)) + self.assertRepr(IndexModel("background", background=True)) + self.assertRepr(IndexModel("sparse", sparse=True)) + self.assertRepr(IndexModel("bucketSize", bucketSize=1)) + self.assertRepr(IndexModel("min", min=1)) + self.assertRepr(IndexModel("max", max=1)) + self.assertRepr(IndexModel("expireAfterSeconds", expireAfterSeconds=1)) + self.assertRepr( + IndexModel("partialFilterExpression", partialFilterExpression={"hello": "world"}) + ) + self.assertRepr(IndexModel("collation", collation=Collation(locale="en_US"))) + self.assertRepr(IndexModel("wildcardProjection", wildcardProjection={"$**": 1})) + self.assertRepr(IndexModel("hidden", hidden=False)) + # Test string literal + self.assertEqual(repr(IndexModel("hello")), "IndexModel({'hello': 1}, name='hello_1')") + self.assertEqual( + repr(IndexModel({"hello": 1, "world": -1})), + "IndexModel({'hello': 1, 'world': -1}, name='hello_1_world_-1')", + ) + + +class TestSearchIndexModel(TestOperationsBase): + """Test SearchIndexModel features.""" + + def test_repr(self): + self.assertRepr(SearchIndexModel({"hello": "hello"}, key=1)) + self.assertEqual( + repr(SearchIndexModel({"hello": "hello"}, key=1)), + "SearchIndexModel(definition={'hello': 'hello'}, key=1)", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_read_write_concern_spec.py b/test/test_read_write_concern_spec.py index 67943d495d..db53b67ae4 100644 --- a/test/test_read_write_concern_spec.py +++ b/test/test_read_write_concern_spec.py @@ -24,7 +24,7 @@ from test import IntegrationTest, client_context, unittest from test.unified_format import generate_test_classes -from test.utils import EventListener +from test.utils import OvertCommandListener from pymongo import DESCENDING from pymongo.errors import ( @@ -44,7 +44,7 @@ class TestReadWriteConcernSpec(IntegrationTest): def test_omit_default_read_write_concern(self): - listener = EventListener() + listener = OvertCommandListener() # Client with default readConcern and writeConcern client = self.rs_or_single_client(event_listeners=[listener]) self.addCleanup(client.close) @@ -205,7 +205,7 @@ def test_error_includes_errInfo(self): @client_context.require_version_min(4, 9) def test_write_error_details_exposes_errinfo(self): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener]) self.addCleanup(client.close) db = client.errinfotest diff --git a/test/test_server_selection.py b/test/test_server_selection.py index 67e9716bf4..984b967f50 100644 --- a/test/test_server_selection.py +++ b/test/test_server_selection.py @@ -33,6 +33,7 @@ from test.utils import ( EventListener, FunctionCallRecorder, + OvertCommandListener, wait_until, ) from test.utils_selection_tests import ( @@ -74,7 +75,7 @@ def custom_selector(servers): return [servers[idx]] # Initialize client with appropriate listeners. - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client( server_selector=custom_selector, event_listeners=[listener] ) diff --git a/test/test_session.py b/test/test_session.py index 980d9df688..634efa11c0 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -36,6 +36,7 @@ from test.utils import ( EventListener, ExceptionCatchingThread, + OvertCommandListener, wait_until, ) @@ -191,7 +192,7 @@ def test_implicit_sessions_checkout(self): lsid_set = set() failures = 0 for _ in range(5): - listener = EventListener() + listener = OvertCommandListener() client = self.rs_or_single_client(event_listeners=[listener], maxPoolSize=1) cursor = client.db.test.find({}) ops: List[Tuple[Callable, List[Any]]] = [ diff --git a/test/test_ssl.py b/test/test_ssl.py index 36d7ba12b6..04db9b61a4 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -33,6 +33,7 @@ ) from test.utils import ( EventListener, + OvertCommandListener, cat_files, ignore_deprecations, ) diff --git a/test/unified_format.py b/test/unified_format.py index 7d5c4e4e03..a88c51e6d5 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -769,7 +769,7 @@ def _databaseOperation_listCollections(self, target, *args, **kwargs): if "batch_size" in kwargs: kwargs["cursor"] = {"batchSize": kwargs.pop("batch_size")} cursor = target.list_collections(*args, **kwargs) - return list(cursor) + return cursor.to_list() def _databaseOperation_createCollection(self, target, *args, **kwargs): # PYTHON-1936 Ignore the listCollections event from create_collection. diff --git a/test/utils.py b/test/utils.py index 24673b698e..493d6f9422 100644 --- a/test/utils.py +++ b/test/utils.py @@ -967,10 +967,6 @@ def parse_spec_options(opts): def prepare_spec_arguments(spec, arguments, opname, entity_map, with_txn_callback): for arg_name in list(arguments): c2s = camel_to_snake(arg_name) - # PyMongo accepts sort as list of tuples. - if arg_name == "sort": - sort_dict = arguments[arg_name] - arguments[arg_name] = list(sort_dict.items()) # Named "key" instead not fieldName. if arg_name == "fieldName": arguments["key"] = arguments.pop(arg_name) diff --git a/tools/synchro.py b/tools/synchro.py index 17841d3025..47617365f4 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -197,6 +197,7 @@ def async_only_test(f: str) -> bool: "test_client_context.py", "test_collation.py", "test_collection.py", + "test_collection_management.py", "test_command_logging.py", "test_command_logging.py", "test_command_monitoring.py", @@ -204,6 +205,7 @@ def async_only_test(f: str) -> bool: "test_common.py", "test_connection_logging.py", "test_connections_survive_primary_stepdown_spec.py", + "test_create_entities.py", "test_crud_unified.py", "test_cursor.py", "test_database.py", From cd69b3627bd916da4cd5900fd5d986a533764746 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 25 Oct 2024 09:14:31 -0400 Subject: [PATCH 07/14] Fix test failures (#1970) --- .evergreen/config.yml | 1427 ++++++++++------- .evergreen/scripts/generate_config.py | 214 ++- pymongo/asynchronous/mongo_client.py | 7 + pymongo/asynchronous/monitor.py | 29 +- pymongo/asynchronous/pool.py | 2 + pymongo/network_layer.py | 3 +- pymongo/synchronous/mongo_client.py | 7 + pymongo/synchronous/monitor.py | 5 + pymongo/synchronous/pool.py | 2 + test/__init__.py | 4 + test/asynchronous/__init__.py | 4 + test/asynchronous/test_client.py | 2 +- ...nnections_survive_primary_stepdown_spec.py | 3 - test/asynchronous/unified_format.py | 5 - test/asynchronous/utils_spec_runner.py | 2 - ...nnections_survive_primary_stepdown_spec.py | 3 - test/unified_format.py | 5 - test/utils_spec_runner.py | 2 - 18 files changed, 1146 insertions(+), 580 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 9083da145b..e357f02f2b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2112,47 +2112,6 @@ axes: AUTH: "noauth" SSL: "nossl" - # Choice of MongoDB server version - - id: mongodb-version - display_name: "MongoDB" - values: - - id: "4.0" - display_name: "MongoDB 4.0" - variables: - VERSION: "4.0" - - id: "4.2" - display_name: "MongoDB 4.2" - variables: - VERSION: "4.2" - - id: "4.4" - display_name: "MongoDB 4.4" - variables: - VERSION: "4.4" - - id: "5.0" - display_name: "MongoDB 5.0" - variables: - VERSION: "5.0" - - id: "6.0" - display_name: "MongoDB 6.0" - variables: - VERSION: "6.0" - - id: "7.0" - display_name: "MongoDB 7.0" - variables: - VERSION: "7.0" - - id: "8.0" - display_name: "MongoDB 8.0" - variables: - VERSION: "8.0" - - id: "latest" - display_name: "MongoDB latest" - variables: - VERSION: "latest" - - id: "rapid" - display_name: "MongoDB rapid" - variables: - VERSION: "rapid" - # Choice of Python runtime version - id: python-version display_name: "Python" @@ -2212,132 +2171,8 @@ axes: variables: PYTHON_BINARY: "C:/python/Python313/python.exe" - - id: python-version-windows-32 - display_name: "Python" - values: - - - - id: "3.9" - display_name: "32-bit Python 3.9" - variables: - PYTHON_BINARY: "C:/python/32/Python39/python.exe" - - id: "3.10" - display_name: "32-bit Python 3.10" - variables: - PYTHON_BINARY: "C:/python/32/Python310/python.exe" - - id: "3.11" - display_name: "32-bit Python 3.11" - variables: - PYTHON_BINARY: "C:/python/32/Python311/python.exe" - - id: "3.12" - display_name: "32-bit Python 3.12" - variables: - PYTHON_BINARY: "C:/python/32/Python312/python.exe" - - id: "3.13" - display_name: "32-bit Python 3.13" - variables: - PYTHON_BINARY: "C:/python/32/Python313/python.exe" - - # Choice of mod_wsgi version - - id: mod-wsgi-version - display_name: "mod_wsgi version" - values: - - id: "4" - display_name: "mod_wsgi 4.x" - variables: - MOD_WSGI_VERSION: "4" - - # Choice of Python async framework - - id: green-framework - display_name: "Green Framework" - values: - - id: "eventlet" - display_name: "Eventlet" - variables: - GREEN_FRAMEWORK: "eventlet" - - id: "gevent" - display_name: "Gevent" - variables: - GREEN_FRAMEWORK: "gevent" - - # Install and use the driver's C-extensions? - - id: c-extensions - display_name: "C Extensions" - values: - - id: "without-c-extensions" - display_name: "Without C Extensions" - variables: - NO_EXT: "1" - - id: "with-c-extensions" - display_name: "With C Extensions" - variables: - NO_EXT: "" - - # Choice of MongoDB storage engine - - id: storage-engine - display_name: Storage - values: - - id: mmapv1 - display_name: MMAPv1 - variables: - STORAGE_ENGINE: "mmapv1" - - id: inmemory - display_name: InMemory - variables: - STORAGE_ENGINE: "inmemory" - - # Run with test commands disabled on server? - - id: disableTestCommands - display_name: Disable test commands - values: - - id: disabled - display_name: disabled - variables: - DISABLE_TEST_COMMANDS: "1" - - # Generate coverage report? - - id: coverage - display_name: "Coverage" - values: - - id: "coverage" - display_name: "Coverage" - tags: ["coverage_tag"] - variables: - COVERAGE: "coverage" - - - id: versionedApi - display_name: "versionedApi" - values: - # Test against a cluster with requireApiVersion=1. - - id: "requireApiVersion1" - display_name: "requireApiVersion1" - tags: [ "versionedApi_tag" ] - variables: - # REQUIRE_API_VERSION is set to make drivers-evergreen-tools - # start a cluster with the requireApiVersion parameter. - REQUIRE_API_VERSION: "1" - # MONGODB_API_VERSION is the apiVersion to use in the test suite. - MONGODB_API_VERSION: "1" - # Test against a cluster with acceptApiVersion2 but without - # requireApiVersion, and don't automatically add apiVersion to - # clients created in the test suite. - - id: "acceptApiVersion2" - display_name: "acceptApiVersion2" - tags: [ "versionedApi_tag" ] - variables: - ORCHESTRATION_FILE: "versioned-api-testing.json" - - - id: serverless - display_name: "Serverless" - values: - - id: "enabled" - display_name: "Serverless" - variables: - test_serverless: true - batchtime: 10080 # 7 days - buildvariants: -# Server Tests for RHEL8. +# Server Tests. - name: test-rhel8-py3.9-auth-ssl-cov tasks: - name: .standalone @@ -2504,8 +2339,6 @@ buildvariants: AUTH: auth SSL: ssl PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 - -# Server tests for MacOS. - name: test-macos-py3.9-auth-ssl-sync tasks: - name: .standalone @@ -2516,8 +2349,32 @@ buildvariants: AUTH: auth SSL: ssl TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-ssl-sync + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-nossl-sync + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth NoSSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 - name: test-macos-py3.9-auth-ssl-async tasks: - name: .standalone @@ -2528,11 +2385,47 @@ buildvariants: AUTH: auth SSL: ssl TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth SSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-nossl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth NoSSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 Auth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - name: test-macos-py3.13-noauth-ssl-sync tasks: - - name: .replica_set + - name: .sharded_cluster display_name: Test macOS py3.13 NoAuth SSL Sync run_on: - macos-14 @@ -2540,46 +2433,56 @@ buildvariants: AUTH: noauth SSL: ssl TEST_SUITES: default - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 SKIP_CSOT_TESTS: "true" -- name: test-macos-py3.13-noauth-ssl-async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-nossl-sync tasks: - - name: .replica_set - display_name: Test macOS py3.13 NoAuth SSL Async + - name: .sharded_cluster + display_name: Test macOS py3.13 NoAuth NoSSL Sync run_on: - macos-14 expansions: AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 Auth SSL Async + run_on: + - macos-14 + expansions: + AUTH: auth SSL: ssl TEST_SUITES: default_async - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 SKIP_CSOT_TESTS: "true" -- name: test-macos-py3.9-noauth-nossl-sync + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-ssl-async tasks: - name: .sharded_cluster - display_name: Test macOS py3.9 NoAuth NoSSL Sync + display_name: Test macOS py3.13 NoAuth SSL Async run_on: - macos-14 expansions: AUTH: noauth - SSL: nossl - TEST_SUITES: default - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + SSL: ssl + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" -- name: test-macos-py3.9-noauth-nossl-async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-nossl-async tasks: - name: .sharded_cluster - display_name: Test macOS py3.9 NoAuth NoSSL Async + display_name: Test macOS py3.13 NoAuth NoSSL Async run_on: - macos-14 expansions: AUTH: noauth SSL: nossl TEST_SUITES: default_async - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 SKIP_CSOT_TESTS: "true" - -# Server tests for macOS Arm64. + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - name: test-macos-arm64-py3.9-auth-ssl-sync tasks: - name: .standalone .6.0 @@ -2596,6 +2499,38 @@ buildvariants: TEST_SUITES: default SKIP_CSOT_TESTS: "true" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-ssl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 - name: test-macos-arm64-py3.9-auth-ssl-async tasks: - name: .standalone .6.0 @@ -2612,123 +2547,161 @@ buildvariants: TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.13-noauth-ssl-sync +- name: test-macos-arm64-py3.9-noauth-ssl-async tasks: - - name: .replica_set .6.0 - - name: .replica_set .7.0 - - name: .replica_set .8.0 - - name: .replica_set .rapid - - name: .replica_set .latest - display_name: Test macOS Arm64 py3.13 NoAuth SSL Sync + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth SSL Async run_on: - macos-14-arm64 expansions: AUTH: noauth SSL: ssl - TEST_SUITES: default + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-noauth-ssl-async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-async tasks: - - name: .replica_set .6.0 - - name: .replica_set .7.0 - - name: .replica_set .8.0 - - name: .replica_set .rapid - - name: .replica_set .latest - display_name: Test macOS Arm64 py3.13 NoAuth SSL Async + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Async run_on: - macos-14-arm64 expansions: AUTH: noauth - SSL: ssl + SSL: nossl TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 Auth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.9-noauth-nossl-sync +- name: test-macos-arm64-py3.13-noauth-ssl-sync tasks: - name: .sharded_cluster .6.0 - name: .sharded_cluster .7.0 - name: .sharded_cluster .8.0 - name: .sharded_cluster .rapid - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Sync + display_name: Test macOS Arm64 py3.13 NoAuth SSL Sync run_on: - macos-14-arm64 expansions: AUTH: noauth - SSL: nossl + SSL: ssl TEST_SUITES: default SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-noauth-nossl-async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-nossl-sync tasks: - name: .sharded_cluster .6.0 - name: .sharded_cluster .7.0 - name: .sharded_cluster .8.0 - name: .sharded_cluster .rapid - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Async + display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Sync run_on: - macos-14-arm64 expansions: AUTH: noauth SSL: nossl - TEST_SUITES: default_async + TEST_SUITES: default SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 - -# Server tests for Windows. -- name: test-win64-py3.9-auth-ssl-sync + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-auth-ssl-async tasks: - - name: .standalone - display_name: Test Win64 py3.9 Auth SSL Sync + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 Auth SSL Async run_on: - - windows-64-vsMulti-small + - macos-14-arm64 expansions: AUTH: auth SSL: ssl - TEST_SUITES: default - PYTHON_BINARY: C:/python/Python39/python.exe + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" -- name: test-win64-py3.9-auth-ssl-async + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-ssl-async tasks: - - name: .standalone - display_name: Test Win64 py3.9 Auth SSL Async + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth SSL Async run_on: - - windows-64-vsMulti-small + - macos-14-arm64 expansions: - AUTH: auth + AUTH: noauth SSL: ssl TEST_SUITES: default_async - PYTHON_BINARY: C:/python/Python39/python.exe SKIP_CSOT_TESTS: "true" -- name: test-win64-py3.13-noauth-ssl-sync + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-nossl-async tasks: - - name: .replica_set - display_name: Test Win64 py3.13 NoAuth SSL Sync + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Async run_on: - - windows-64-vsMulti-small + - macos-14-arm64 expansions: AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-win64-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth SSL: ssl TEST_SUITES: default - PYTHON_BINARY: C:/python/Python313/python.exe SKIP_CSOT_TESTS: "true" -- name: test-win64-py3.13-noauth-ssl-async + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-ssl-sync tasks: - - name: .replica_set - display_name: Test Win64 py3.13 NoAuth SSL Async + - name: .standalone + display_name: Test Win64 py3.9 NoAuth SSL Sync run_on: - windows-64-vsMulti-small expansions: AUTH: noauth SSL: ssl - TEST_SUITES: default_async - PYTHON_BINARY: C:/python/Python313/python.exe + TEST_SUITES: default SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe - name: test-win64-py3.9-noauth-nossl-sync tasks: - - name: .sharded_cluster + - name: .standalone display_name: Test Win64 py3.9 NoAuth NoSSL Sync run_on: - windows-64-vsMulti-small @@ -2736,11 +2709,35 @@ buildvariants: AUTH: noauth SSL: nossl TEST_SUITES: default + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe - name: test-win64-py3.9-noauth-nossl-async tasks: - - name: .sharded_cluster + - name: .standalone display_name: Test Win64 py3.9 NoAuth NoSSL Async run_on: - windows-64-vsMulti-small @@ -2748,8 +2745,80 @@ buildvariants: AUTH: noauth SSL: nossl TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe - name: test-win32-py3.9-auth-ssl-sync tasks: - name: .standalone @@ -2760,10 +2829,32 @@ buildvariants: AUTH: auth SSL: ssl TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-nossl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default SKIP_CSOT_TESTS: "true" - -# Server tests for Win32. + PYTHON_BINARY: C:/python/32/Python39/python.exe - name: test-win32-py3.9-auth-ssl-async tasks: - name: .standalone @@ -2774,11 +2865,47 @@ buildvariants: AUTH: auth SSL: ssl TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-nossl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe - name: test-win32-py3.13-noauth-ssl-sync tasks: - - name: .replica_set + - name: .sharded_cluster display_name: Test Win32 py3.13 NoAuth SSL Sync run_on: - windows-64-vsMulti-small @@ -2786,44 +2913,56 @@ buildvariants: AUTH: noauth SSL: ssl TEST_SUITES: default - PYTHON_BINARY: C:/python/32/Python313/python.exe SKIP_CSOT_TESTS: "true" -- name: test-win32-py3.13-noauth-ssl-async + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-nossl-sync tasks: - - name: .replica_set - display_name: Test Win32 py3.13 NoAuth SSL Async + - name: .sharded_cluster + display_name: Test Win32 py3.13 NoAuth NoSSL Sync run_on: - windows-64-vsMulti-small expansions: AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth SSL: ssl TEST_SUITES: default_async - PYTHON_BINARY: C:/python/32/Python313/python.exe SKIP_CSOT_TESTS: "true" -- name: test-win32-py3.9-noauth-nossl-sync + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-ssl-async tasks: - name: .sharded_cluster - display_name: Test Win32 py3.9 NoAuth NoSSL Sync + display_name: Test Win32 py3.13 NoAuth SSL Async run_on: - windows-64-vsMulti-small expansions: AUTH: noauth - SSL: nossl - TEST_SUITES: default - PYTHON_BINARY: C:/python/32/Python39/python.exe + SSL: ssl + TEST_SUITES: default_async SKIP_CSOT_TESTS: "true" -- name: test-win32-py3.9-noauth-nossl-async + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-nossl-async tasks: - name: .sharded_cluster - display_name: Test Win32 py3.9 NoAuth NoSSL Async + display_name: Test Win32 py3.13 NoAuth NoSSL Async run_on: - windows-64-vsMulti-small expansions: AUTH: noauth SSL: nossl TEST_SUITES: default_async - PYTHON_BINARY: C:/python/32/Python39/python.exe SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe # Encryption tests. - name: encryption-rhel8-py3.9-auth-ssl @@ -3168,203 +3307,587 @@ buildvariants: run_on: - rhel87-small expansions: - COMPRESSORS: zstd - NO_EXT: "1" + COMPRESSORS: zstd + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: zstd-compression-rhel8-py3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: snappy-compression-rhel8-pypy3.9 + tasks: + - name: .standalone + display_name: snappy compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: zlib-compression-rhel8-pypy3.10 + tasks: + - name: .standalone + display_name: zlib compression RHEL8 pypy3.10 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: zstd-compression-rhel8-pypy3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 + +# Enterprise auth tests. +- name: enterprise-auth-macos-py3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth macOS py3.9 Auth + run_on: + - macos-14 + expansions: + AUTH: auth + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: enterprise-auth-rhel8-py3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: enterprise-auth-rhel8-py3.11-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.11 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: enterprise-auth-rhel8-py3.12-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.12 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: enterprise-auth-win64-py3.13-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth Win64 py3.13 Auth + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + PYTHON_BINARY: C:/python/Python313/python.exe +- name: enterprise-auth-rhel8-pypy3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: enterprise-auth-rhel8-pypy3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + +# PyOpenSSL tests. +- name: pyopenssl-macos-py3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL macOS py3.9 + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: noauth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: pyopenssl-rhel8-py3.10 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: pyopenssl-rhel8-py3.11 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.11 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: pyopenssl-rhel8-py3.12 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.12 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: pyopenssl-win64-py3.13 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL Win64 py3.13 + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: C:/python/Python313/python.exe +- name: pyopenssl-rhel8-pypy3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.9 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: pyopenssl-rhel8-pypy3.10 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + +# Storage Engine tests. +- name: storage-inmemory-rhel8-py3.9 + tasks: + - name: .standalone .4.0 + - name: .standalone .4.4 + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Storage InMemory RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + STORAGE_ENGINE: inmemory + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: storage-mmapv1-rhel8-py3.9 + tasks: + - name: .standalone .4.0 + - name: .replica_set .4.0 + display_name: Storage MMAPv1 RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + STORAGE_ENGINE: mmapv1 + PYTHON_BINARY: /opt/python/3.9/bin/python3 + +# Versioned API tests. +- name: versioned-api-require-v1-rhel8-py3.9-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API require v1 RHEL8 py3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + REQUIRE_API_VERSION: "1" + MONGODB_API_VERSION: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-accept-v2-rhel8-py3.9-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API accept v2 RHEL8 py3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + ORCHESTRATION_FILE: versioned-api-testing.json + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-require-v1-rhel8-py3.13-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API require v1 RHEL8 py3.13 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + REQUIRE_API_VERSION: "1" + MONGODB_API_VERSION: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-accept-v2-rhel8-py3.13-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API accept v2 RHEL8 py3.13 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + ORCHESTRATION_FILE: versioned-api-testing.json PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: zstd-compression-rhel8-py3.9 + tags: [versionedApi_tag] + +# Green framework tests. +- name: eventlet-rhel8-py3.9 tasks: - - name: .standalone !.4.0 - display_name: zstd compression RHEL8 py3.9 + - name: .standalone + display_name: Eventlet RHEL8 py3.9 run_on: - rhel87-small expansions: - COMPRESSORS: zstd + GREEN_FRAMEWORK: eventlet + AUTH: auth + SSL: ssl PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: snappy-compression-rhel8-pypy3.9 +- name: gevent-rhel8-py3.9 tasks: - name: .standalone - display_name: snappy compression RHEL8 pypy3.9 + display_name: Gevent RHEL8 py3.9 run_on: - rhel87-small expansions: - COMPRESSORS: snappy - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: zlib-compression-rhel8-pypy3.10 + GREEN_FRAMEWORK: gevent + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: eventlet-rhel8-py3.12 tasks: - name: .standalone - display_name: zlib compression RHEL8 pypy3.10 + display_name: Eventlet RHEL8 py3.12 run_on: - rhel87-small expansions: - COMPRESSORS: zlib - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- name: zstd-compression-rhel8-pypy3.9 + GREEN_FRAMEWORK: eventlet + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: gevent-rhel8-py3.12 tasks: - - name: .standalone !.4.0 - display_name: zstd compression RHEL8 pypy3.9 + - name: .standalone + display_name: Gevent RHEL8 py3.12 run_on: - rhel87-small expansions: - COMPRESSORS: zstd - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 + GREEN_FRAMEWORK: gevent + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 -# Enterprise auth tests. -- name: enterprise-auth-macos-py3.9-auth +# No C Ext tests. +- name: no-c-ext-rhel8-py3.9 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth macOS py3.9 Auth + - name: .standalone + display_name: No C Ext RHEL8 py3.9 run_on: - - macos-14 + - rhel87-small expansions: - AUTH: auth - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: enterprise-auth-rhel8-py3.10-auth + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: no-c-ext-rhel8-py3.10 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.10 Auth + - name: .replica_set + display_name: No C Ext RHEL8 py3.10 run_on: - rhel87-small expansions: - AUTH: auth + NO_EXT: "1" PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: enterprise-auth-rhel8-py3.11-auth +- name: no-c-ext-rhel8-py3.11 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.11 Auth + - name: .sharded_cluster + display_name: No C Ext RHEL8 py3.11 run_on: - rhel87-small expansions: - AUTH: auth + NO_EXT: "1" PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: enterprise-auth-rhel8-py3.12-auth +- name: no-c-ext-rhel8-py3.12 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.12 Auth + - name: .standalone + display_name: No C Ext RHEL8 py3.12 run_on: - rhel87-small expansions: - AUTH: auth + NO_EXT: "1" PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: enterprise-auth-win64-py3.13-auth +- name: no-c-ext-rhel8-py3.13 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth Win64 py3.13 Auth + - name: .replica_set + display_name: No C Ext RHEL8 py3.13 run_on: - - windows-64-vsMulti-small + - rhel87-small expansions: - AUTH: auth - PYTHON_BINARY: C:/python/Python313/python.exe -- name: enterprise-auth-rhel8-pypy3.9-auth + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Atlas Data Lake tests. +- name: atlas-data-lake-rhel8-py3.9-no-c tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 pypy3.9 Auth + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.9 No C run_on: - rhel87-small expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: enterprise-auth-rhel8-pypy3.10-auth + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: atlas-data-lake-rhel8-py3.9 tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 pypy3.10 Auth + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.9 run_on: - rhel87-small expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - -# PyOpenSSL tests. -- name: pyopenssl-macos-py3.9 + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: atlas-data-lake-rhel8-py3.13-no-c tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL macOS py3.9 + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.13 No C run_on: - - macos-14 - batchtime: 10080 + - rhel87-small expansions: - AUTH: noauth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: pyopenssl-rhel8-py3.10 + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: atlas-data-lake-rhel8-py3.13 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.10 + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.13 run_on: - rhel87-small - batchtime: 10080 expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: pyopenssl-rhel8-py3.11 + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Mod_wsgi tests. +- name: mod_wsgi-ubuntu-22-py3.9 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.11 + - name: mod-wsgi-standalone + - name: mod-wsgi-replica-set + - name: mod-wsgi-embedded-mode-standalone + - name: mod-wsgi-embedded-mode-replica-set + display_name: mod_wsgi Ubuntu-22 py3.9 run_on: - - rhel87-small - batchtime: 10080 + - ubuntu2204-small expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: pyopenssl-rhel8-py3.12 + MOD_WSGI_VERSION: "4" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: mod_wsgi-ubuntu-22-py3.13 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.12 + - name: mod-wsgi-standalone + - name: mod-wsgi-replica-set + - name: mod-wsgi-embedded-mode-standalone + - name: mod-wsgi-embedded-mode-replica-set + display_name: mod_wsgi Ubuntu-22 py3.13 run_on: - - rhel87-small - batchtime: 10080 + - ubuntu2204-small expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: pyopenssl-win64-py3.13 + MOD_WSGI_VERSION: "4" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Disable test commands variants. +- name: disable-test-commands-rhel8-py3.9 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL Win64 py3.13 + - name: .latest + display_name: Disable test commands RHEL8 py3.9 run_on: - - windows-64-vsMulti-small - batchtime: 10080 + - rhel87-small expansions: AUTH: auth - test_pyopenssl: "true" SSL: ssl - PYTHON_BINARY: C:/python/Python313/python.exe -- name: pyopenssl-rhel8-pypy3.9 + DISABLE_TEST_COMMANDS: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + +# Serverless variants. +- name: serverless-rhel8-py3.9 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 pypy3.9 + - name: serverless_task_group + display_name: Serverless RHEL8 py3.9 run_on: - rhel87-small batchtime: 10080 expansions: + test_serverless: "true" AUTH: auth - test_pyopenssl: "true" SSL: ssl - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: pyopenssl-rhel8-pypy3.10 + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: serverless-rhel8-py3.13 tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 pypy3.10 + - name: serverless_task_group + display_name: Serverless RHEL8 py3.13 run_on: - rhel87-small batchtime: 10080 expansions: + test_serverless: "true" AUTH: auth - test_pyopenssl: "true" SSL: ssl - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# AWS Auth tests. +- name: aws-auth-ubuntu-20-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Ubuntu-20 py3.9 + run_on: + - ubuntu2004-small + expansions: + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: aws-auth-ubuntu-20-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Ubuntu-20 py3.13 + run_on: + - ubuntu2004-small + expansions: + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: aws-auth-win64-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Win64 py3.9 + run_on: + - windows-64-vsMulti-small + expansions: + skip_ECS_auth_test: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: aws-auth-win64-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Win64 py3.13 + run_on: + - windows-64-vsMulti-small + expansions: + skip_ECS_auth_test: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: aws-auth-macos-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth macOS py3.9 + run_on: + - macos-14 + expansions: + skip_ECS_auth_test: "true" + skip_EC2_auth_test: "true" + skip_web_identity_auth_test: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: aws-auth-macos-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth macOS py3.13 + run_on: + - macos-14 + expansions: + skip_ECS_auth_test: "true" + skip_EC2_auth_test: "true" + skip_web_identity_auth_test: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - matrix_name: "tests-fips" matrix_spec: @@ -3388,47 +3911,6 @@ buildvariants: tasks: - ".6.0" -- matrix_name: "tests-python-version-rhel8-without-c-extensions" - matrix_spec: - platform: rhel8 - python-version: "*" - c-extensions: without-c-extensions - auth-ssl: noauth-nossl - coverage: "*" - exclude_spec: - # These interpreters are always tested without extensions. - - platform: rhel8 - python-version: ["pypy3.9", "pypy3.10"] - c-extensions: "*" - auth-ssl: "*" - coverage: "*" - display_name: "${c-extensions} ${python-version} ${platform} ${auth} ${ssl} ${coverage}" - tasks: &all-server-versions - - ".rapid" - - ".latest" - - ".8.0" - - ".7.0" - - ".6.0" - - ".5.0" - - ".4.4" - - ".4.2" - - ".4.0" - -- matrix_name: "tests-python-version-green-framework-rhel8" - matrix_spec: - platform: rhel8 - python-version: "*" - green-framework: "*" - auth-ssl: "*" - exclude_spec: - # Don't test green frameworks on these Python versions. - - platform: rhel8 - python-version: ["pypy3.9", "pypy3.10", "3.13"] - green-framework: "*" - auth-ssl: "*" - display_name: "${green-framework} ${python-version} ${platform} ${auth-ssl}" - tasks: *all-server-versions - - matrix_name: "tests-python-version-supports-openssl-102-test-ssl" matrix_spec: platform: rhel7 @@ -3439,48 +3921,6 @@ buildvariants: tasks: - ".5.0" -# Storage engine tests on RHEL 8.4 (x86_64) with Python 3.9. -- matrix_name: "tests-storage-engines" - matrix_spec: - platform: rhel8 - storage-engine: "*" - python-version: "3.9" - display_name: "Storage ${storage-engine} ${python-version} ${platform}" - rules: - - if: - platform: rhel8 - storage-engine: ["inmemory"] - python-version: "*" - then: - add_tasks: - - "test-latest-standalone" - - "test-8.0-standalone" - - "test-7.0-standalone" - - "test-6.0-standalone" - - "test-5.0-standalone" - - "test-4.4-standalone" - - "test-4.2-standalone" - - "test-4.0-standalone" - - if: - # MongoDB 4.2 drops support for MMAPv1 - platform: rhel8 - storage-engine: ["mmapv1"] - python-version: "*" - then: - add_tasks: - - "test-4.0-standalone" - - "test-4.0-replica_set" - -# enableTestCommands=0 tests on RHEL 8.4 (x86_64) with Python 3.9. -- matrix_name: "test-disableTestCommands" - matrix_spec: - platform: rhel8 - disableTestCommands: "*" - python-version: "3.9" - display_name: "Disable test commands ${python-version} ${platform}" - tasks: - - ".latest" - - matrix_name: "test-search-index-helpers" matrix_spec: platform: rhel8 @@ -3489,18 +3929,6 @@ buildvariants: tasks: - name: "test_atlas_task_group_search_indexes" -- matrix_name: "tests-mod-wsgi" - matrix_spec: - platform: ubuntu-22.04 - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - mod-wsgi-version: "*" - display_name: "${mod-wsgi-version} ${python-version} ${platform}" - tasks: - - name: "mod-wsgi-standalone" - - name: "mod-wsgi-replica-set" - - name: "mod-wsgi-embedded-mode-standalone" - - name: "mod-wsgi-embedded-mode-replica-set" - - matrix_name: "mockupdb-tests" matrix_spec: platform: rhel8 @@ -3539,42 +3967,6 @@ buildvariants: tasks: - name: "atlas-connect" -- matrix_name: "serverless" - matrix_spec: - platform: rhel8 - python-version: "*" - auth-ssl: auth-ssl - serverless: "enabled" - display_name: "${serverless} ${python-version} ${platform}" - tasks: - - "serverless_task_group" - -- matrix_name: "data-lake-spec-tests" - matrix_spec: - platform: ubuntu-22.04 - python-version: ["3.9", "3.10"] - auth: "auth" - c-extensions: "*" - display_name: "Atlas Data Lake ${python-version} ${c-extensions}" - tasks: - - name: atlas-data-lake-tests - -- matrix_name: "stable-api-tests" - matrix_spec: - platform: rhel8 - python-version: ["3.9", "3.10"] - auth: "auth" - versionedApi: "*" - display_name: "Versioned API ${versionedApi} ${python-version}" - batchtime: 10080 # 7 days - tasks: - # Versioned API was introduced in MongoDB 4.7 - - "test-latest-standalone" - - "test-8.0-standalone" - - "test-7.0-standalone" - - "test-6.0-standalone" - - "test-5.0-standalone" - # OCSP test matrix. - name: ocsp-test-rhel8-v4.4-py3.9 tasks: @@ -3939,47 +4331,6 @@ buildvariants: - name: testgcpoidc_task_group batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README -- matrix_name: "aws-auth-test" - matrix_spec: - platform: [ubuntu-20.04] - python-version: ["3.9"] - display_name: "MONGODB-AWS Auth ${platform} ${python-version}" - tasks: - - name: "aws-auth-test-4.4" - - name: "aws-auth-test-5.0" - - name: "aws-auth-test-6.0" - - name: "aws-auth-test-7.0" - - name: "aws-auth-test-8.0" - - name: "aws-auth-test-rapid" - - name: "aws-auth-test-latest" - -- matrix_name: "aws-auth-test-mac" - matrix_spec: - platform: [macos] - display_name: "MONGODB-AWS Auth ${platform} ${python-version-mac}" - tasks: - - name: "aws-auth-test-4.4" - - name: "aws-auth-test-5.0" - - name: "aws-auth-test-6.0" - - name: "aws-auth-test-7.0" - - name: "aws-auth-test-8.0" - - name: "aws-auth-test-rapid" - - name: "aws-auth-test-latest" - -- matrix_name: "aws-auth-test-windows" - matrix_spec: - platform: [windows] - python-version-windows: "*" - display_name: "MONGODB-AWS Auth ${platform} ${python-version-windows}" - tasks: - - name: "aws-auth-test-4.4" - - name: "aws-auth-test-5.0" - - name: "aws-auth-test-6.0" - - name: "aws-auth-test-7.0" - - name: "aws-auth-test-8.0" - - name: "aws-auth-test-rapid" - - name: "aws-auth-test-latest" - - name: testgcpkms-variant display_name: "GCP KMS" run_on: diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index 6d614a9afe..3f1ea724ed 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -23,7 +23,6 @@ ############## ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"] -VERSIONS_6_0_PLUS = ["6.0", "7.0", "8.0", "rapid", "latest"] CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] PYPYS = ["pypy3.9", "pypy3.10"] ALL_PYTHONS = CPYTHONS + PYPYS @@ -55,6 +54,8 @@ class Host: HOSTS["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32") HOSTS["macos"] = Host("macos", "macos-14", "macOS") HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64") +HOSTS["ubuntu20"] = Host("ubuntu20", "ubuntu2004-small", "Ubuntu-20") +HOSTS["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22") ############## @@ -103,7 +104,7 @@ def get_python_binary(python: str, host: str) -> str: python = python.replace(".", "") return f"{base}/Python{python}/python.exe" - if host == "rhel8": + if host in ["rhel8", "ubuntu22", "ubuntu20"]: return f"/opt/python/{python}/bin/python3" if host in ["macos", "macos-arm64"]: @@ -112,6 +113,24 @@ def get_python_binary(python: str, host: str) -> str: raise ValueError(f"no match found for python {python} on {host}") +def get_versions_from(min_version: str) -> list[str]: + """Get all server versions starting from a minimum version.""" + min_version_float = float(min_version) + rapid_latest = ["rapid", "latest"] + versions = [v for v in ALL_VERSIONS if v not in rapid_latest] + return [v for v in versions if float(v) >= min_version_float] + rapid_latest + + +def get_versions_until(max_version: str) -> list[str]: + """Get all server version up to a max version.""" + max_version_float = float(max_version) + versions = [v for v in ALL_VERSIONS if v not in ["rapid", "latest"]] + versions = [v for v in versions if float(v) <= max_version_float] + if not len(versions): + raise ValueError(f"No server versions found less <= {max_version}") + return versions + + def get_display_name(base: str, host: str, **kwargs) -> str: """Get the display name of a variant.""" display_name = f"{base} {HOSTS[host].display_name}" @@ -236,14 +255,17 @@ def create_server_variants() -> list[BuildVariant]: # Test a subset on each of the other platforms. for host in ("macos", "macos-arm64", "win64", "win32"): - for (python, (auth, ssl), topology), sync in product( - zip_cycle(MIN_MAX_PYTHON, AUTH_SSLS, TOPOLOGIES), SYNCS - ): + for ( + python, + sync, + (auth, ssl), + ) in product(MIN_MAX_PYTHON, SYNCS, AUTH_SSLS): test_suite = "default" if sync == "sync" else "default_async" + topology = TOPOLOGIES[0] if python == CPYTHONS[0] else TOPOLOGIES[-1] tasks = [f".{topology}"] # MacOS arm64 only works on server versions 6.0+ if host == "macos-arm64": - tasks = [f".{topology} .{version}" for version in VERSIONS_6_0_PLUS] + tasks = [f".{topology} .{version}" for version in get_versions_from("6.0")] expansions = dict(AUTH=auth, SSL=ssl, TEST_SUITES=test_suite, SKIP_CSOT_TESTS="true") display_name = get_display_name("Test", host, python=python, **expansions) variant = create_variant( @@ -330,7 +352,7 @@ def create_load_balancer_variants(): task_names = ["load-balancer-test"] batchtime = BATCHTIME_WEEK expansions_base = dict(test_loadbalancer="true") - versions = ["6.0", "7.0", "8.0", "latest", "rapid"] + versions = get_versions_from("6.0") variants = [] pythons = CPYTHONS + PYPYS for ind, (version, (auth, ssl)) in enumerate(product(versions, AUTH_SSLS)): @@ -442,10 +464,186 @@ def create_pyopenssl_variants(): return variants +def create_storage_engine_tests(): + host = "rhel8" + engines = ["InMemory", "MMAPv1"] + variants = [] + for engine in engines: + python = CPYTHONS[0] + expansions = dict(STORAGE_ENGINE=engine.lower()) + if engine == engines[0]: + tasks = [f".standalone .{v}" for v in ALL_VERSIONS] + else: + # MongoDB 4.2 drops support for MMAPv1 + versions = get_versions_until("4.0") + tasks = [f".standalone .{v}" for v in versions] + [ + f".replica_set .{v}" for v in versions + ] + display_name = get_display_name(f"Storage {engine}", host, python=python) + variant = create_variant( + tasks, display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + return variants + + +def create_versioned_api_tests(): + host = "rhel8" + tags = ["versionedApi_tag"] + tasks = [f".standalone .{v}" for v in get_versions_from("5.0")] + variants = [] + types = ["require v1", "accept v2"] + + # All python versions across platforms. + for python, test_type in product(MIN_MAX_PYTHON, types): + expansions = dict(AUTH="auth") + # Test against a cluster with requireApiVersion=1. + if test_type == types[0]: + # REQUIRE_API_VERSION is set to make drivers-evergreen-tools + # start a cluster with the requireApiVersion parameter. + expansions["REQUIRE_API_VERSION"] = "1" + # MONGODB_API_VERSION is the apiVersion to use in the test suite. + expansions["MONGODB_API_VERSION"] = "1" + else: + # Test against a cluster with acceptApiVersion2 but without + # requireApiVersion, and don't automatically add apiVersion to + # clients created in the test suite. + expansions["ORCHESTRATION_FILE"] = "versioned-api-testing.json" + base_display_name = f"Versioned API {test_type}" + display_name = get_display_name(base_display_name, host, python=python, **expansions) + variant = create_variant( + tasks, display_name, host=host, python=python, tags=tags, expansions=expansions + ) + variants.append(variant) + + return variants + + +def create_green_framework_variants(): + variants = [] + tasks = [".standalone"] + host = "rhel8" + for python, framework in product([CPYTHONS[0], CPYTHONS[-2]], ["eventlet", "gevent"]): + expansions = dict(GREEN_FRAMEWORK=framework, AUTH="auth", SSL="ssl") + display_name = get_display_name(f"{framework.capitalize()}", host, python=python) + variant = create_variant( + tasks, display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + return variants + + +def generate_no_c_ext_variants(): + variants = [] + host = "rhel8" + for python, topology in zip_cycle(CPYTHONS, TOPOLOGIES): + tasks = [f".{topology}"] + expansions = dict() + handle_c_ext(C_EXTS[0], expansions) + display_name = get_display_name("No C Ext", host, python=python) + variant = create_variant( + tasks, display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + return variants + + +def generate_atlas_data_lake_variants(): + variants = [] + host = "rhel8" + for python, c_ext in product(MIN_MAX_PYTHON, C_EXTS): + tasks = ["atlas-data-lake-tests"] + expansions = dict() + handle_c_ext(c_ext, expansions) + display_name = get_display_name("Atlas Data Lake", host, python=python, **expansions) + variant = create_variant( + tasks, display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + return variants + + +def generate_mod_wsgi_variants(): + variants = [] + host = "ubuntu22" + tasks = [ + "mod-wsgi-standalone", + "mod-wsgi-replica-set", + "mod-wsgi-embedded-mode-standalone", + "mod-wsgi-embedded-mode-replica-set", + ] + expansions = dict(MOD_WSGI_VERSION="4") + for python in MIN_MAX_PYTHON: + display_name = get_display_name("mod_wsgi", host, python=python) + variant = create_variant( + tasks, display_name, host=host, python=python, expansions=expansions + ) + variants.append(variant) + return variants + + +def generate_disable_test_commands_variants(): + host = "rhel8" + expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1") + python = CPYTHONS[0] + display_name = get_display_name("Disable test commands", host, python=python) + tasks = [".latest"] + return [create_variant(tasks, display_name, host=host, python=python, expansions=expansions)] + + +def generate_serverless_variants(): + host = "rhel8" + batchtime = BATCHTIME_WEEK + expansions = dict(test_serverless="true", AUTH="auth", SSL="ssl") + tasks = ["serverless_task_group"] + base_name = "Serverless" + return [ + create_variant( + tasks, + get_display_name(base_name, host, python=python), + host=host, + python=python, + expansions=expansions, + batchtime=batchtime, + ) + for python in MIN_MAX_PYTHON + ] + + +def generate_aws_auth_variants(): + variants = [] + tasks = [ + "aws-auth-test-4.4", + "aws-auth-test-5.0", + "aws-auth-test-6.0", + "aws-auth-test-7.0", + "aws-auth-test-8.0", + "aws-auth-test-rapid", + "aws-auth-test-latest", + ] + + for host, python in product(["ubuntu20", "win64", "macos"], MIN_MAX_PYTHON): + expansions = dict() + if host != "ubuntu20": + expansions["skip_ECS_auth_test"] = "true" + if host == "macos": + expansions["skip_EC2_auth_test"] = "true" + expansions["skip_web_identity_auth_test"] = "true" + variant = create_variant( + tasks, + get_display_name("AWS Auth", host, python=python), + host=host, + python=python, + expansions=expansions, + ) + variants.append(variant) + return variants + + ################## # Generate Config ################## -variants = create_pyopenssl_variants() +variants = create_server_variants() # print(len(variants)) generate_yaml(variants=variants) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index a33246a24b..c63d0fc3fc 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -32,6 +32,7 @@ """ from __future__ import annotations +import asyncio import contextlib import os import warnings @@ -2036,6 +2037,8 @@ async def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: await self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2050,6 +2053,8 @@ async def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: await self._kill_cursors(cursor_ids, address, topology, session=None) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2064,6 +2069,8 @@ async def _process_periodic_tasks(self) -> None: try: await self._process_kill_cursors() await self._topology.update_pool() + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index bbfd6a2998..780704fabb 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -16,6 +16,7 @@ from __future__ import annotations +import asyncio import atexit import logging import time @@ -26,7 +27,7 @@ from pymongo._csot import MovingMinimum from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled from pymongo.hello import Hello -from pymongo.lock import _create_lock +from pymongo.lock import _async_create_lock from pymongo.logger import _SDAM_LOGGER, _debug_log, _SDAMStatusMessage from pymongo.periodic_executor import _shutdown_executors from pymongo.pool_options import _is_faas @@ -276,7 +277,7 @@ async def _check_server(self) -> ServerDescription: await self._reset_connection() if isinstance(error, _OperationCancelled): raise - self._rtt_monitor.reset() + await self._rtt_monitor.reset() # Server type defaults to Unknown. return ServerDescription(address, error=error) @@ -315,9 +316,9 @@ async def _check_once(self) -> ServerDescription: self._cancel_context = conn.cancel_context response, round_trip_time = await self._check_with_socket(conn) if not response.awaitable: - self._rtt_monitor.add_sample(round_trip_time) + await self._rtt_monitor.add_sample(round_trip_time) - avg_rtt, min_rtt = self._rtt_monitor.get() + avg_rtt, min_rtt = await self._rtt_monitor.get() sd = ServerDescription(address, response, avg_rtt, min_round_trip_time=min_rtt) if self._publish: assert self._listeners is not None @@ -413,6 +414,8 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception + except asyncio.CancelledError: + raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -441,7 +444,7 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool self._pool = pool self._moving_average = MovingAverage() self._moving_min = MovingMinimum() - self._lock = _create_lock() + self._lock = _async_create_lock() async def close(self) -> None: self.gc_safe_close() @@ -449,20 +452,20 @@ async def close(self) -> None: # thread has the socket checked out, it will be closed when checked in. await self._pool.reset() - def add_sample(self, sample: float) -> None: + async def add_sample(self, sample: float) -> None: """Add a RTT sample.""" - with self._lock: + async with self._lock: self._moving_average.add_sample(sample) self._moving_min.add_sample(sample) - def get(self) -> tuple[Optional[float], float]: + async def get(self) -> tuple[Optional[float], float]: """Get the calculated average, or None if no samples yet and the min.""" - with self._lock: + async with self._lock: return self._moving_average.get(), self._moving_min.get() - def reset(self) -> None: + async def reset(self) -> None: """Reset the average RTT.""" - with self._lock: + async with self._lock: self._moving_average.reset() self._moving_min.reset() @@ -472,10 +475,12 @@ async def _run(self) -> None: # heartbeat protocol (MongoDB 4.4+). # XXX: Skip check if the server is unknown? rtt = await self._ping() - self.add_sample(rtt) + await self.add_sample(rtt) except ReferenceError: # Topology was garbage-collected. await self.close() + except asyncio.CancelledError: + raise except Exception: await self._pool.reset() diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index 2fe9579aef..a37aa3b46a 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -704,6 +704,8 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() + except asyncio.CancelledError: + raise except Exception: # noqa: S110 pass diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index aa16e85a07..377689047b 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -271,7 +271,8 @@ async def async_receive_data( ) for task in pending: task.cancel() - await asyncio.wait(pending) + if pending: + await asyncio.wait(pending) if len(done) == 0: raise socket.timeout("timed out") if read_task in done: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index eb363f82f5..9c98a47037 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -32,6 +32,7 @@ """ from __future__ import annotations +import asyncio import contextlib import os import warnings @@ -2030,6 +2031,8 @@ def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2044,6 +2047,8 @@ def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: self._kill_cursors(cursor_ids, address, topology, session=None) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2058,6 +2063,8 @@ def _process_periodic_tasks(self) -> None: try: self._process_kill_cursors() self._topology.update_pool() + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index a806670f2c..d82a5f976d 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -16,6 +16,7 @@ from __future__ import annotations +import asyncio import atexit import logging import time @@ -413,6 +414,8 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception + except asyncio.CancelledError: + raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -476,6 +479,8 @@ def _run(self) -> None: except ReferenceError: # Topology was garbage-collected. self.close() + except asyncio.CancelledError: + raise except Exception: self._pool.reset() diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 6ac7b4eca9..99201b822e 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -702,6 +702,8 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() + except asyncio.CancelledError: + raise except Exception: # noqa: S110 pass diff --git a/test/__init__.py b/test/__init__.py index c1944f5870..dba3312424 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -17,6 +17,7 @@ import asyncio import gc +import logging import multiprocessing import os import signal @@ -25,6 +26,7 @@ import sys import threading import time +import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -191,6 +193,8 @@ def _connect(self, host, port, **kwargs): client.close() def _init_client(self): + self.mongoses = [] + self.connection_attempts = [] self.client = self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 9ca5a32ffc..bed49de161 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -17,6 +17,7 @@ import asyncio import gc +import logging import multiprocessing import os import signal @@ -25,6 +26,7 @@ import sys import threading import time +import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -191,6 +193,8 @@ async def _connect(self, host, port, **kwargs): await client.close() async def _init_client(self): + self.mongoses = [] + self.connection_attempts = [] self.client = await self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 47cbff6d5b..292a78d645 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -2580,7 +2580,7 @@ async def test_direct_client_maintains_pool_to_arbiter(self): await async_wait_until(lambda: len(c.nodes) == 1, "connect") self.assertEqual(await c.address, ("c", 3)) # Assert that we create 1 pooled connection. - listener.wait_for_event(monitoring.ConnectionReadyEvent, 1) + await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 1) self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1) arbiter = c._topology.get_server_by_address(("c", 3)) self.assertEqual(len(arbiter.pool.conns), 1) diff --git a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py index ffff428379..bc9638b443 100644 --- a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py +++ b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py @@ -44,9 +44,6 @@ class TestAsyncConnectionsSurvivePrimaryStepDown(AsyncIntegrationTest): listener: CMAPListener coll: AsyncCollection - async def asyncTearDown(self): - await reset_client_context() - @async_client_context.require_replica_set async def asyncSetUp(self): self.listener = CMAPListener() diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 11b124a124..e8d1e4380f 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -530,11 +530,6 @@ async def asyncSetUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) - async def asyncTearDown(self): - for client in self.mongos_clients: - await client.close() - await super().asyncTearDown() - def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if async_client_context.storage_engine == "mmapv1": diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index f0463244d7..75aa50b578 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -264,8 +264,6 @@ async def asyncSetUp(self) -> None: async def asyncTearDown(self) -> None: self.knobs.disable() - for client in self.mongos_clients: - await client.close() async def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) diff --git a/test/test_connections_survive_primary_stepdown_spec.py b/test/test_connections_survive_primary_stepdown_spec.py index 4387850a00..84ef6decd5 100644 --- a/test/test_connections_survive_primary_stepdown_spec.py +++ b/test/test_connections_survive_primary_stepdown_spec.py @@ -44,9 +44,6 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest): listener: CMAPListener coll: Collection - def tearDown(self): - reset_client_context() - @client_context.require_replica_set def setUp(self): self.listener = CMAPListener() diff --git a/test/unified_format.py b/test/unified_format.py index a88c51e6d5..435078989b 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -529,11 +529,6 @@ def setUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) - def tearDown(self): - for client in self.mongos_clients: - client.close() - super().tearDown() - def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if client_context.storage_engine == "mmapv1": diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index 682cf0b0f8..3dea4ede1c 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -264,8 +264,6 @@ def setUp(self) -> None: def tearDown(self) -> None: self.knobs.disable() - for client in self.mongos_clients: - client.close() def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) From 2d98fb1f8e0aa079479764ad15661136b136e05c Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 25 Oct 2024 12:13:16 -0400 Subject: [PATCH 08/14] Fix test failures (#1973) --- test/asynchronous/test_auth_spec.py | 2 +- test/asynchronous/test_change_stream.py | 2 +- test/asynchronous/test_connection_logging.py | 2 +- test/asynchronous/test_create_entities.py | 6 ++++++ test/asynchronous/test_crud_unified.py | 2 +- test/asynchronous/test_encryption.py | 2 +- test/asynchronous/unified_format.py | 2 -- test/test_create_entities.py | 6 ++++++ test/unified_format.py | 2 -- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/test/asynchronous/test_auth_spec.py b/test/asynchronous/test_auth_spec.py index a6ab1cb331..e9e43d5759 100644 --- a/test/asynchronous/test_auth_spec.py +++ b/test/asynchronous/test_auth_spec.py @@ -25,7 +25,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes from pymongo import AsyncMongoClient from pymongo.asynchronous.auth_oidc import OIDCCallback diff --git a/test/asynchronous/test_change_stream.py b/test/asynchronous/test_change_stream.py index 873631bbe5..08da00cc1e 100644 --- a/test/asynchronous/test_change_stream.py +++ b/test/asynchronous/test_change_stream.py @@ -35,7 +35,7 @@ async_client_context, unittest, ) -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, EventListener, diff --git a/test/asynchronous/test_connection_logging.py b/test/asynchronous/test_connection_logging.py index 6bc9835b70..945c6c59b5 100644 --- a/test/asynchronous/test_connection_logging.py +++ b/test/asynchronous/test_connection_logging.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_create_entities.py b/test/asynchronous/test_create_entities.py index cb2ec63f4c..1f68cf6ddc 100644 --- a/test/asynchronous/test_create_entities.py +++ b/test/asynchronous/test_create_entities.py @@ -56,6 +56,9 @@ async def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + await client.close() async def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -122,6 +125,9 @@ async def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + await client.close() if __name__ == "__main__": diff --git a/test/asynchronous/test_crud_unified.py b/test/asynchronous/test_crud_unified.py index 3d8deb36e9..e6f42d5bdf 100644 --- a/test/asynchronous/test_crud_unified.py +++ b/test/asynchronous/test_crud_unified.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index d75bad6862..ba68960c5e 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -46,6 +46,7 @@ unittest, ) from test.asynchronous.test_bulk import AsyncBulkTestBase +from test.asynchronous.unified_format import generate_test_classes from test.asynchronous.utils_spec_runner import AsyncSpecRunner from test.helpers import ( AWS_CREDS, @@ -56,7 +57,6 @@ KMIP_CREDS, LOCAL_MASTER_KEY, ) -from test.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, OvertCommandListener, diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index e8d1e4380f..ec70c1dc13 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -304,7 +304,6 @@ async def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = await self.test.async_rs_or_single_client(**kwargs) self[spec["id"]] = client - self.test.addAsyncCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -1037,7 +1036,6 @@ async def _testOperation_targetedFailPoint(self, spec): ) client = await self.async_single_client("{}:{}".format(*session._pinned_address)) - self.addAsyncCleanup(client.close) await self.__set_fail_point(client=client, command_args=spec["failPoint"]) async def _testOperation_createEntities(self, spec): diff --git a/test/test_create_entities.py b/test/test_create_entities.py index ad75fe5702..9d77a08eee 100644 --- a/test/test_create_entities.py +++ b/test/test_create_entities.py @@ -56,6 +56,9 @@ def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + client.close() def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -122,6 +125,9 @@ def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + client.close() if __name__ == "__main__": diff --git a/test/unified_format.py b/test/unified_format.py index 435078989b..6fea541e8a 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -303,7 +303,6 @@ def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = self.test.rs_or_single_client(**kwargs) self[spec["id"]] = client - self.test.addCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -1028,7 +1027,6 @@ def _testOperation_targetedFailPoint(self, spec): ) client = self.single_client("{}:{}".format(*session._pinned_address)) - self.addCleanup(client.close) self.__set_fail_point(client=client, command_args=spec["failPoint"]) def _testOperation_createEntities(self, spec): From 3d64532a66072c471256fc1a587baf50131c1bc8 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 4 Nov 2024 15:00:53 -0500 Subject: [PATCH 09/14] Revert Fix test failures commits (#1995) --- .evergreen/config.yml | 2747 ++++++++++++++++- .evergreen/scripts/generate_config.py | 547 +--- pymongo/asynchronous/mongo_client.py | 7 - pymongo/asynchronous/monitor.py | 29 +- pymongo/asynchronous/pool.py | 2 - pymongo/network_layer.py | 3 +- pymongo/synchronous/mongo_client.py | 7 - pymongo/synchronous/monitor.py | 5 - pymongo/synchronous/pool.py | 2 - test/__init__.py | 4 - test/asynchronous/__init__.py | 4 - test/asynchronous/test_auth_spec.py | 2 +- test/asynchronous/test_change_stream.py | 2 +- test/asynchronous/test_client.py | 2 +- test/asynchronous/test_connection_logging.py | 2 +- ...nnections_survive_primary_stepdown_spec.py | 3 + test/asynchronous/test_create_entities.py | 6 - test/asynchronous/test_crud_unified.py | 2 +- test/asynchronous/test_encryption.py | 2 +- test/asynchronous/unified_format.py | 7 + test/asynchronous/utils_spec_runner.py | 2 + ...nnections_survive_primary_stepdown_spec.py | 3 + test/test_create_entities.py | 6 - test/unified_format.py | 7 + test/utils_spec_runner.py | 2 + 25 files changed, 2765 insertions(+), 640 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index fc1713a88e..e357f02f2b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -25,10 +25,6 @@ timeout: script: | ls -la -include: - - filename: .evergreen/generated_configs/tasks.yml - - filename: .evergreen/generated_configs/variants.yml - functions: "fetch source": # Executes clone and applies the submitted patch, if any @@ -520,18 +516,6 @@ functions: args: - .evergreen/run-mongodb-oidc-test.sh - "run oidc k8s auth test": - - command: subprocess.exec - type: test - params: - binary: bash - working_dir: src - env: - OIDC_ENV: k8s - include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "K8S_VARIANT"] - args: - - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh - "run aws auth test with aws credentials as environment variables": - command: shell.exec type: test @@ -885,32 +869,6 @@ task_groups: tasks: - oidc-auth-test-gcp - - name: testk8soidc_task_group - setup_group: - - func: fetch source - - func: prepare resources - - func: fix absolute paths - - func: make files executable - - command: ec2.assume_role - params: - role_arn: ${aws_test_secrets_role} - duration_seconds: 1800 - - command: subprocess.exec - params: - binary: bash - args: - - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh - teardown_task: - - command: subprocess.exec - params: - binary: bash - args: - - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh - setup_group_can_fail_task: true - setup_group_timeout_secs: 1800 - tasks: - - oidc-auth-test-k8s - - name: testoidc_task_group setup_group: - func: fetch source @@ -1010,6 +968,249 @@ tasks: TOPOLOGY: "server" - func: "run doctests" + - name: "test-4.0-standalone" + tags: ["4.0", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.0" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-4.0-replica_set" + tags: ["4.0", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.0" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-4.0-sharded_cluster" + tags: ["4.0", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.0" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-4.2-standalone" + tags: ["4.2", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.2" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-4.2-replica_set" + tags: ["4.2", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.2" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-4.2-sharded_cluster" + tags: ["4.2", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.2" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-4.4-standalone" + tags: ["4.4", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.4" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-4.4-replica_set" + tags: ["4.4", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.4" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-4.4-sharded_cluster" + tags: ["4.4", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "4.4" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-5.0-standalone" + tags: ["5.0", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "5.0" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-5.0-replica_set" + tags: ["5.0", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "5.0" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-5.0-sharded_cluster" + tags: ["5.0", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "5.0" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-6.0-standalone" + tags: ["6.0", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "6.0" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-6.0-replica_set" + tags: ["6.0", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "6.0" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-6.0-sharded_cluster" + tags: ["6.0", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "6.0" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-8.0-standalone" + tags: ["8.0", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "8.0" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-8.0-replica_set" + tags: ["8.0", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "8.0" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-8.0-sharded_cluster" + tags: ["8.0", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "8.0" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-7.0-standalone" + tags: ["7.0", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "7.0" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-7.0-replica_set" + tags: ["7.0", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "7.0" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-7.0-sharded_cluster" + tags: ["7.0", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "7.0" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-latest-standalone" + tags: ["latest", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "latest" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-latest-replica_set" + tags: ["latest", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "latest" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-latest-sharded_cluster" + tags: ["latest", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "latest" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + + - name: "test-rapid-standalone" + tags: ["rapid", "standalone"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "rapid" + TOPOLOGY: "server" + - func: "run tests" + + - name: "test-rapid-replica_set" + tags: ["rapid", "replica_set"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "rapid" + TOPOLOGY: "replica_set" + - func: "run tests" + + - name: "test-rapid-sharded_cluster" + tags: ["rapid", "sharded_cluster"] + commands: + - func: "bootstrap mongo-orchestration" + vars: + VERSION: "rapid" + TOPOLOGY: "sharded_cluster" + - func: "run tests" + - name: "test-serverless" tags: ["serverless"] commands: @@ -1580,47 +1781,64 @@ tasks: - func: "run aws auth test with aws web identity credentials" - func: "run aws ECS auth test" + - name: load-balancer-test + commands: + - func: "bootstrap mongo-orchestration" + vars: + TOPOLOGY: "sharded_cluster" + LOAD_BALANCER: true + - func: "run load-balancer" + - func: "run tests" + - name: "oidc-auth-test" commands: - func: "run oidc auth test with test credentials" - name: "oidc-auth-test-azure" commands: - - command: subprocess.exec + - command: shell.exec type: test params: - binary: bash - working_dir: src - env: - OIDC_ENV: azure - include_expansions_in_env: ["DRIVERS_TOOLS"] - args: - - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh + shell: bash + script: |- + set -o errexit + . src/.evergreen/scripts/env.sh + cd src + git add . + git commit -m "add files" + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz + git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD + export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh - name: "oidc-auth-test-gcp" commands: - - command: subprocess.exec + - command: shell.exec type: test params: - binary: bash - working_dir: src - env: - OIDC_ENV: gcp - include_expansions_in_env: ["DRIVERS_TOOLS"] - args: - - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh - - - name: "oidc-auth-test-k8s" + shell: bash + script: |- + set -o errexit + . src/.evergreen/scripts/env.sh + cd src + git add . + git commit -m "add files" + export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz + git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD + # Define the command to run on the VM. + # Ensure that we source the environment file created for us, set up any other variables we need, + # and then run our test suite on the vm. + export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + + - name: "test-fips-standalone" + tags: ["fips"] commands: - - func: "run oidc k8s auth test" - vars: - K8S_VARIANT: eks - - func: "run oidc k8s auth test" - vars: - K8S_VARIANT: gke - - func: "run oidc k8s auth test" + - func: "bootstrap mongo-orchestration" vars: - K8S_VARIANT: aks + VERSION: "latest" + TOPOLOGY: "server" + - func: "run tests" # }}} - name: "coverage-report" tags: ["coverage"] @@ -1736,6 +1954,23 @@ tasks: - func: "attach benchmark test results" - func: "send dashboard data" + - name: "assign-pr-reviewer" + tags: ["pr"] + allowed_requesters: ["patch", "github_pr"] + commands: + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: src + script: | + . .evergreen/scripts/env.sh + set -x + export CONFIG=$PROJECT_DIRECTORY/.github/reviewers.txt + export SCRIPT="$DRIVERS_TOOLS/.evergreen/github_app/assign-reviewer.sh" + bash $SCRIPT -p $CONFIG -h ${github_commit} -o "mongodb" -n "mongo-python-driver" + echo '{"results": [{ "status": "PASS", "test_file": "Build", "log_raw": "Test completed" } ]}' > ${PROJECT_DIRECTORY}/test-results.json + - name: "check-import-time" tags: ["pr"] commands: @@ -1764,41 +1999,2369 @@ tasks: - mongo-python-driver - ${github_commit} +axes: + # Choice of distro + - id: platform + display_name: OS + values: + - id: macos + display_name: "macOS" + run_on: macos-14 + variables: + skip_EC2_auth_test: true + skip_ECS_auth_test: true + skip_web_identity_auth_test: true + # CSOT tests are unreliable on our slow macOS hosts. + SKIP_CSOT_TESTS: true + - id: macos-arm64 + display_name: "macOS Arm64" + run_on: macos-14-arm64 + variables: + skip_EC2_auth_test: true + skip_ECS_auth_test: true + skip_web_identity_auth_test: true + # CSOT tests are unreliable on our slow macOS hosts. + SKIP_CSOT_TESTS: true + - id: rhel7 + display_name: "RHEL 7.x" + run_on: rhel79-small + batchtime: 10080 # 7 days + - id: rhel8 + display_name: "RHEL 8.x" + run_on: rhel8.8-small + batchtime: 10080 # 7 days + - id: rhel9-fips + display_name: "RHEL 9 FIPS" + run_on: rhel92-fips + batchtime: 10080 # 7 days + - id: ubuntu-22.04 + display_name: "Ubuntu 22.04" + run_on: ubuntu2204-small + batchtime: 10080 # 7 days + - id: ubuntu-20.04 + display_name: "Ubuntu 20.04" + run_on: ubuntu2004-small + batchtime: 10080 # 7 days + - id: rhel8-zseries + display_name: "RHEL 8 (zSeries)" + run_on: rhel8-zseries-small + batchtime: 10080 # 7 days + variables: + SKIP_HATCH: true + - id: rhel8-power8 + display_name: "RHEL 8 (POWER8)" + run_on: rhel8-power-small + batchtime: 10080 # 7 days + variables: + SKIP_HATCH: true + - id: rhel8-arm64 + display_name: "RHEL 8 (ARM64)" + run_on: rhel82-arm64-small + batchtime: 10080 # 7 days + variables: + - id: windows + display_name: "Windows 64" + run_on: windows-64-vsMulti-small + batchtime: 10080 # 7 days + variables: + skip_ECS_auth_test: true + skip_EC2_auth_test: true + skip_web_identity_auth_test: true + venv_bin_dir: "Scripts" + # CSOT tests are unreliable on our slow Windows hosts. + SKIP_CSOT_TESTS: true + + # Test with authentication? + - id: auth + display_name: Authentication + values: + - id: auth + display_name: Auth + variables: + AUTH: "auth" + - id: noauth + display_name: NoAuth + variables: + AUTH: "noauth" + + # Test with SSL? + - id: ssl + display_name: SSL + values: + - id: ssl + display_name: SSL + variables: + SSL: "ssl" + - id: nossl + display_name: NoSSL + variables: + SSL: "nossl" + + # Test with Auth + SSL (combined for convenience)? + - id: auth-ssl + display_name: Auth SSL + values: + - id: auth-ssl + display_name: Auth SSL + variables: + AUTH: "auth" + SSL: "ssl" + - id: noauth-nossl + display_name: NoAuth NoSSL + variables: + AUTH: "noauth" + SSL: "nossl" + + # Choice of Python runtime version + - id: python-version + display_name: "Python" + values: + # Note: always display platform with python-version to avoid ambiguous display names. + # Linux + - id: "3.9" + display_name: "Python 3.9" + variables: + PYTHON_BINARY: "/opt/python/3.9/bin/python3" + - id: "3.10" + display_name: "Python 3.10" + variables: + PYTHON_BINARY: "/opt/python/3.10/bin/python3" + - id: "3.11" + display_name: "Python 3.11" + variables: + PYTHON_BINARY: "/opt/python/3.11/bin/python3" + - id: "3.12" + display_name: "Python 3.12" + variables: + PYTHON_BINARY: "/opt/python/3.12/bin/python3" + - id: "3.13" + display_name: "Python 3.13" + variables: + PYTHON_BINARY: "/opt/python/3.13/bin/python3" + - id: "pypy3.9" + display_name: "PyPy 3.9" + variables: + PYTHON_BINARY: "/opt/python/pypy3.9/bin/pypy3" + - id: "pypy3.10" + display_name: "PyPy 3.10" + variables: + PYTHON_BINARY: "/opt/python/pypy3.10/bin/pypy3" + + - id: python-version-windows + display_name: "Python" + values: + - id: "3.9" + display_name: "Python 3.9" + variables: + PYTHON_BINARY: "C:/python/Python39/python.exe" + - id: "3.10" + display_name: "Python 3.10" + variables: + PYTHON_BINARY: "C:/python/Python310/python.exe" + - id: "3.11" + display_name: "Python 3.11" + variables: + PYTHON_BINARY: "C:/python/Python311/python.exe" + - id: "3.12" + display_name: "Python 3.12" + variables: + PYTHON_BINARY: "C:/python/Python312/python.exe" + - id: "3.13" + display_name: "Python 3.13" + variables: + PYTHON_BINARY: "C:/python/Python313/python.exe" + buildvariants: -- name: "no-server" - display_name: "No server" +# Server Tests. +- name: test-rhel8-py3.9-auth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 Auth SSL cov run_on: - - rhel84-small + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.9-noauth-ssl-cov tasks: - - name: "no-server" - -- name: "Coverage Report" - display_name: "Coverage Report" + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 NoAuth SSL cov run_on: - - rhel84-small + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.9-noauth-nossl-cov tasks: - - name: "coverage-report" - -- name: testkms-variant - display_name: "KMS" + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.9 NoAuth NoSSL cov run_on: - - debian11-small + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-auth-ssl-cov tasks: - - name: testgcpkms_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - - testgcpkms-fail-task - - name: testazurekms_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - - testazurekms-fail-task - -- name: rhel8-test-lambda - display_name: FaaS Lambda + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 Auth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-noauth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 NoAuth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.13-noauth-nossl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 py3.13 NoAuth NoSSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-auth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 Auth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-noauth-ssl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 NoAuth SSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-pypy3.10-noauth-nossl-cov + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Test RHEL8 pypy3.10 NoAuth NoSSL cov + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + COVERAGE: coverage + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [coverage_tag] +- name: test-rhel8-py3.10-auth-ssl + tasks: + - name: .standalone + display_name: Test RHEL8 py3.10 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: test-rhel8-py3.11-noauth-ssl + tasks: + - name: .replica_set + display_name: Test RHEL8 py3.11 NoAuth SSL + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: ssl + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: test-rhel8-py3.12-noauth-nossl + tasks: + - name: .sharded_cluster + display_name: Test RHEL8 py3.12 NoAuth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: noauth + SSL: nossl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: test-rhel8-pypy3.9-auth-ssl + tasks: + - name: .standalone + display_name: Test RHEL8 pypy3.9 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: test-macos-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test macOS py3.9 Auth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-ssl-sync + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-nossl-sync + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth NoSSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 Auth SSL Async + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth SSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.9-noauth-nossl-async + tasks: + - name: .standalone + display_name: Test macOS py3.9 NoAuth NoSSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 Auth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 NoAuth SSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 NoAuth NoSSL Sync + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 Auth SSL Async + run_on: + - macos-14 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 NoAuth SSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-py3.13-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test macOS py3.13 NoAuth NoSSL Async + run_on: + - macos-14 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.9-auth-ssl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 Auth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-ssl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-sync + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-auth-ssl-async + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 Auth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-ssl-async + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.9-noauth-nossl-async + tasks: + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: test-macos-arm64-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 Auth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-ssl-sync + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth SSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-nossl-sync + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Sync + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 Auth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-ssl-async + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth SSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-macos-arm64-py3.13-noauth-nossl-async + tasks: + - name: .sharded_cluster .6.0 + - name: .sharded_cluster .7.0 + - name: .sharded_cluster .8.0 + - name: .sharded_cluster .rapid + - name: .sharded_cluster .latest + display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Async + run_on: + - macos-14-arm64 + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 +- name: test-win64-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win64 py3.9 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-nossl-sync + tasks: + - name: .standalone + display_name: Test Win64 py3.9 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.9-noauth-nossl-async + tasks: + - name: .standalone + display_name: Test Win64 py3.9 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: test-win64-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win64-py3.13-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test Win64 py3.13 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: test-win32-py3.9-auth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-ssl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-nossl-sync + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-auth-ssl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-ssl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.9-noauth-nossl-async + tasks: + - name: .standalone + display_name: Test Win32 py3.9 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python39/python.exe +- name: test-win32-py3.13-auth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 Auth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-ssl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 NoAuth SSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-nossl-sync + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 NoAuth NoSSL Sync + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-auth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 Auth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-ssl-async + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 NoAuth SSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: ssl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe +- name: test-win32-py3.13-noauth-nossl-async + tasks: + - name: .sharded_cluster + display_name: Test Win32 py3.13 NoAuth NoSSL Async + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: noauth + SSL: nossl + TEST_SUITES: default_async + SKIP_CSOT_TESTS: "true" + PYTHON_BINARY: C:/python/32/Python313/python.exe + +# Encryption tests. +- name: encryption-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption crypt_shared RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-py3.9-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-py3.13-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 py3.13 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-pyopenssl-rhel8-pypy3.10-auth-ssl + tasks: + - name: .standalone + - name: .replica_set + - name: .sharded_cluster + display_name: Encryption PyOpenSSL RHEL8 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_encryption_pyopenssl: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + tags: [encryption_tag] +- name: encryption-rhel8-py3.10-auth-ssl + tasks: + - name: .replica_set + display_name: Encryption RHEL8 py3.10 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: encryption-crypt_shared-rhel8-py3.11-auth-nossl + tasks: + - name: .replica_set + display_name: Encryption crypt_shared RHEL8 py3.11 Auth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: encryption-pyopenssl-rhel8-py3.12-auth-ssl + tasks: + - name: .replica_set + display_name: Encryption PyOpenSSL RHEL8 py3.12 Auth SSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + TEST_ENCRYPTION_PYOPENSSL: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: encryption-rhel8-pypy3.9-auth-nossl + tasks: + - name: .replica_set + display_name: Encryption RHEL8 pypy3.9 Auth NoSSL + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: encryption-macos-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption macOS py3.9 Auth SSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-macos-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption macOS py3.13 Auth NoSSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-macos-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared macOS py3.9 Auth SSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 + tags: [encryption_tag] +- name: encryption-crypt_shared-macos-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared macOS py3.13 Auth NoSSL + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + tags: [encryption_tag] +- name: encryption-win64-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption Win64 py3.9 Auth SSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + PYTHON_BINARY: C:/python/Python39/python.exe + tags: [encryption_tag] +- name: encryption-win64-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption Win64 py3.13 Auth NoSSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + PYTHON_BINARY: C:/python/Python313/python.exe + tags: [encryption_tag] +- name: encryption-crypt_shared-win64-py3.9-auth-ssl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared Win64 py3.9 Auth SSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: ssl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: C:/python/Python39/python.exe + tags: [encryption_tag] +- name: encryption-crypt_shared-win64-py3.13-auth-nossl + tasks: + - name: .latest .replica_set + display_name: Encryption crypt_shared Win64 py3.13 Auth NoSSL + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + SSL: nossl + test_encryption: "true" + test_crypt_shared: "true" + PYTHON_BINARY: C:/python/Python313/python.exe + tags: [encryption_tag] + +# Compressor tests. +- name: snappy-compression-rhel8-py3.9-no-c + tasks: + - name: .standalone + display_name: snappy compression RHEL8 py3.9 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: snappy-compression-rhel8-py3.10 + tasks: + - name: .standalone + display_name: snappy compression RHEL8 py3.10 + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: zlib-compression-rhel8-py3.11-no-c + tasks: + - name: .standalone + display_name: zlib compression RHEL8 py3.11 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: zlib-compression-rhel8-py3.12 + tasks: + - name: .standalone + display_name: zlib compression RHEL8 py3.12 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: zstd-compression-rhel8-py3.13-no-c + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 py3.13 No C + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: zstd-compression-rhel8-py3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: snappy-compression-rhel8-pypy3.9 + tasks: + - name: .standalone + display_name: snappy compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: snappy + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: zlib-compression-rhel8-pypy3.10 + tasks: + - name: .standalone + display_name: zlib compression RHEL8 pypy3.10 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zlib + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: zstd-compression-rhel8-pypy3.9 + tasks: + - name: .standalone !.4.0 + display_name: zstd compression RHEL8 pypy3.9 + run_on: + - rhel87-small + expansions: + COMPRESSORS: zstd + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 + +# Enterprise auth tests. +- name: enterprise-auth-macos-py3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth macOS py3.9 Auth + run_on: + - macos-14 + expansions: + AUTH: auth + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: enterprise-auth-rhel8-py3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: enterprise-auth-rhel8-py3.11-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.11 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: enterprise-auth-rhel8-py3.12-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 py3.12 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: enterprise-auth-win64-py3.13-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth Win64 py3.13 Auth + run_on: + - windows-64-vsMulti-small + expansions: + AUTH: auth + PYTHON_BINARY: C:/python/Python313/python.exe +- name: enterprise-auth-rhel8-pypy3.9-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: enterprise-auth-rhel8-pypy3.10-auth + tasks: + - name: test-enterprise-auth + display_name: Enterprise Auth RHEL8 pypy3.10 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + +# PyOpenSSL tests. +- name: pyopenssl-macos-py3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL macOS py3.9 + run_on: + - macos-14 + batchtime: 10080 + expansions: + AUTH: noauth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: pyopenssl-rhel8-py3.10 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: pyopenssl-rhel8-py3.11 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.11 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: pyopenssl-rhel8-py3.12 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 py3.12 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: pyopenssl-win64-py3.13 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL Win64 py3.13 + run_on: + - windows-64-vsMulti-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: C:/python/Python313/python.exe +- name: pyopenssl-rhel8-pypy3.9 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.9 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: pyopenssl-rhel8-pypy3.10 + tasks: + - name: .replica_set + - name: .7.0 + display_name: PyOpenSSL RHEL8 pypy3.10 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + AUTH: auth + test_pyopenssl: "true" + SSL: ssl + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 + +# Storage Engine tests. +- name: storage-inmemory-rhel8-py3.9 + tasks: + - name: .standalone .4.0 + - name: .standalone .4.4 + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Storage InMemory RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + STORAGE_ENGINE: inmemory + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: storage-mmapv1-rhel8-py3.9 + tasks: + - name: .standalone .4.0 + - name: .replica_set .4.0 + display_name: Storage MMAPv1 RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + STORAGE_ENGINE: mmapv1 + PYTHON_BINARY: /opt/python/3.9/bin/python3 + +# Versioned API tests. +- name: versioned-api-require-v1-rhel8-py3.9-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API require v1 RHEL8 py3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + REQUIRE_API_VERSION: "1" + MONGODB_API_VERSION: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-accept-v2-rhel8-py3.9-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API accept v2 RHEL8 py3.9 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + ORCHESTRATION_FILE: versioned-api-testing.json + PYTHON_BINARY: /opt/python/3.9/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-require-v1-rhel8-py3.13-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API require v1 RHEL8 py3.13 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + REQUIRE_API_VERSION: "1" + MONGODB_API_VERSION: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [versionedApi_tag] +- name: versioned-api-accept-v2-rhel8-py3.13-auth + tasks: + - name: .standalone .5.0 + - name: .standalone .6.0 + - name: .standalone .7.0 + - name: .standalone .8.0 + - name: .standalone .rapid + - name: .standalone .latest + display_name: Versioned API accept v2 RHEL8 py3.13 Auth + run_on: + - rhel87-small + expansions: + AUTH: auth + ORCHESTRATION_FILE: versioned-api-testing.json + PYTHON_BINARY: /opt/python/3.13/bin/python3 + tags: [versionedApi_tag] + +# Green framework tests. +- name: eventlet-rhel8-py3.9 + tasks: + - name: .standalone + display_name: Eventlet RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + GREEN_FRAMEWORK: eventlet + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: gevent-rhel8-py3.9 + tasks: + - name: .standalone + display_name: Gevent RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + GREEN_FRAMEWORK: gevent + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: eventlet-rhel8-py3.12 + tasks: + - name: .standalone + display_name: Eventlet RHEL8 py3.12 + run_on: + - rhel87-small + expansions: + GREEN_FRAMEWORK: eventlet + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: gevent-rhel8-py3.12 + tasks: + - name: .standalone + display_name: Gevent RHEL8 py3.12 + run_on: + - rhel87-small + expansions: + GREEN_FRAMEWORK: gevent + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.12/bin/python3 + +# No C Ext tests. +- name: no-c-ext-rhel8-py3.9 + tasks: + - name: .standalone + display_name: No C Ext RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: no-c-ext-rhel8-py3.10 + tasks: + - name: .replica_set + display_name: No C Ext RHEL8 py3.10 + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: no-c-ext-rhel8-py3.11 + tasks: + - name: .sharded_cluster + display_name: No C Ext RHEL8 py3.11 + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: no-c-ext-rhel8-py3.12 + tasks: + - name: .standalone + display_name: No C Ext RHEL8 py3.12 + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: no-c-ext-rhel8-py3.13 + tasks: + - name: .replica_set + display_name: No C Ext RHEL8 py3.13 + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Atlas Data Lake tests. +- name: atlas-data-lake-rhel8-py3.9-no-c + tasks: + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.9 No C + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: atlas-data-lake-rhel8-py3.9 + tasks: + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: atlas-data-lake-rhel8-py3.13-no-c + tasks: + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.13 No C + run_on: + - rhel87-small + expansions: + NO_EXT: "1" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: atlas-data-lake-rhel8-py3.13 + tasks: + - name: atlas-data-lake-tests + display_name: Atlas Data Lake RHEL8 py3.13 + run_on: + - rhel87-small + expansions: + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Mod_wsgi tests. +- name: mod_wsgi-ubuntu-22-py3.9 + tasks: + - name: mod-wsgi-standalone + - name: mod-wsgi-replica-set + - name: mod-wsgi-embedded-mode-standalone + - name: mod-wsgi-embedded-mode-replica-set + display_name: mod_wsgi Ubuntu-22 py3.9 + run_on: + - ubuntu2204-small + expansions: + MOD_WSGI_VERSION: "4" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: mod_wsgi-ubuntu-22-py3.13 + tasks: + - name: mod-wsgi-standalone + - name: mod-wsgi-replica-set + - name: mod-wsgi-embedded-mode-standalone + - name: mod-wsgi-embedded-mode-replica-set + display_name: mod_wsgi Ubuntu-22 py3.13 + run_on: + - ubuntu2204-small + expansions: + MOD_WSGI_VERSION: "4" + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# Disable test commands variants. +- name: disable-test-commands-rhel8-py3.9 + tasks: + - name: .latest + display_name: Disable test commands RHEL8 py3.9 + run_on: + - rhel87-small + expansions: + AUTH: auth + SSL: ssl + DISABLE_TEST_COMMANDS: "1" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + +# Serverless variants. +- name: serverless-rhel8-py3.9 + tasks: + - name: serverless_task_group + display_name: Serverless RHEL8 py3.9 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + test_serverless: "true" + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: serverless-rhel8-py3.13 + tasks: + - name: serverless_task_group + display_name: Serverless RHEL8 py3.13 + run_on: + - rhel87-small + batchtime: 10080 + expansions: + test_serverless: "true" + AUTH: auth + SSL: ssl + PYTHON_BINARY: /opt/python/3.13/bin/python3 + +# AWS Auth tests. +- name: aws-auth-ubuntu-20-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Ubuntu-20 py3.9 + run_on: + - ubuntu2004-small + expansions: + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: aws-auth-ubuntu-20-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Ubuntu-20 py3.13 + run_on: + - ubuntu2004-small + expansions: + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: aws-auth-win64-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Win64 py3.9 + run_on: + - windows-64-vsMulti-small + expansions: + skip_ECS_auth_test: "true" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: aws-auth-win64-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth Win64 py3.13 + run_on: + - windows-64-vsMulti-small + expansions: + skip_ECS_auth_test: "true" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: aws-auth-macos-py3.9 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth macOS py3.9 + run_on: + - macos-14 + expansions: + skip_ECS_auth_test: "true" + skip_EC2_auth_test: "true" + skip_web_identity_auth_test: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: aws-auth-macos-py3.13 + tasks: + - name: aws-auth-test-4.4 + - name: aws-auth-test-5.0 + - name: aws-auth-test-6.0 + - name: aws-auth-test-7.0 + - name: aws-auth-test-8.0 + - name: aws-auth-test-rapid + - name: aws-auth-test-latest + display_name: AWS Auth macOS py3.13 + run_on: + - macos-14 + expansions: + skip_ECS_auth_test: "true" + skip_EC2_auth_test: "true" + skip_web_identity_auth_test: "true" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + +- matrix_name: "tests-fips" + matrix_spec: + platform: + - rhel9-fips + auth: "auth" + ssl: "ssl" + display_name: "${platform} ${auth} ${ssl}" + tasks: + - "test-fips-standalone" + +# Test one server version with zSeries, POWER8, and ARM. +- matrix_name: "test-different-cpu-architectures" + matrix_spec: + platform: + - rhel8-zseries # Added in 5.0.8 (SERVER-44074) + - rhel8-power8 # Added in 4.2.7 (SERVER-44072) + - rhel8-arm64 # Added in 4.4.2 (SERVER-48282) + auth-ssl: "*" + display_name: "${platform} ${auth-ssl}" + tasks: + - ".6.0" + +- matrix_name: "tests-python-version-supports-openssl-102-test-ssl" + matrix_spec: + platform: rhel7 + # Python 3.10+ requires OpenSSL 1.1.1+ + python-version: ["3.9"] + auth-ssl: "*" + display_name: "OpenSSL 1.0.2 ${python-version} ${platform} ${auth-ssl}" + tasks: + - ".5.0" + +- matrix_name: "test-search-index-helpers" + matrix_spec: + platform: rhel8 + python-version: "3.9" + display_name: "Search Index Helpers ${platform}" + tasks: + - name: "test_atlas_task_group_search_indexes" + +- matrix_name: "mockupdb-tests" + matrix_spec: + platform: rhel8 + python-version: 3.9 + display_name: "MockupDB Tests" + tasks: + - name: "mockupdb" + +- matrix_name: "tests-doctests" + matrix_spec: + platform: rhel8 + python-version: ["3.9"] + display_name: "Doctests ${python-version} ${platform}" + tasks: + - name: "doctests" + +- name: "no-server" + display_name: "No server test" + run_on: + - rhel84-small + tasks: + - name: "no-server" + +- name: "Coverage Report" + display_name: "Coverage Report" + run_on: + - rhel84-small + tasks: + - name: "coverage-report" + +- matrix_name: "atlas-connect" + matrix_spec: + platform: rhel8 + python-version: "*" + display_name: "Atlas connect ${python-version} ${platform}" + tasks: + - name: "atlas-connect" + +# OCSP test matrix. +- name: ocsp-test-rhel8-v4.4-py3.9 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v4.4 py3.9 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: ocsp-test-rhel8-v5.0-py3.10 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v5.0 py3.10 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "5.0" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: ocsp-test-rhel8-v6.0-py3.11 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v6.0 py3.11 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "6.0" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: ocsp-test-rhel8-v7.0-py3.12 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v7.0 py3.12 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "7.0" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: ocsp-test-rhel8-v8.0-py3.13 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 v8.0 py3.13 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: ocsp-test-rhel8-rapid-pypy3.9 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 rapid pypy3.9 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: rapid + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: ocsp-test-rhel8-latest-pypy3.10 + tasks: + - name: .ocsp + display_name: OCSP test RHEL8 latest pypy3.10 + run_on: + - rhel87-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: latest + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: ocsp-test-win64-v4.4-py3.9 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test Win64 v4.4 py3.9 + run_on: + - windows-64-vsMulti-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: C:/python/Python39/python.exe +- name: ocsp-test-win64-v8.0-py3.13 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test Win64 v8.0 py3.13 + run_on: + - windows-64-vsMulti-small + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: C:/python/Python313/python.exe +- name: ocsp-test-macos-v4.4-py3.9 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test macOS v4.4 py3.9 + run_on: + - macos-14 + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "4.4" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 +- name: ocsp-test-macos-v8.0-py3.13 + tasks: + - name: .ocsp-rsa !.ocsp-staple + display_name: OCSP test macOS v8.0 py3.13 + run_on: + - macos-14 + batchtime: 20160 + expansions: + AUTH: noauth + SSL: ssl + TOPOLOGY: server + VERSION: "8.0" + PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 + +# Load balancer tests +- name: load-balancer-rhel8-v6.0-py3.9-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: load-balancer-rhel8-v6.0-py3.10-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.10 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: load-balancer-rhel8-v6.0-py3.11-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v6.0 py3.11 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "6.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: load-balancer-rhel8-v7.0-py3.12-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 py3.12 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: load-balancer-rhel8-v7.0-py3.13-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 py3.13 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: load-balancer-rhel8-v7.0-pypy3.9-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v7.0 pypy3.9 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "7.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: load-balancer-rhel8-v8.0-pypy3.10-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 pypy3.10 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: load-balancer-rhel8-v8.0-py3.9-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 py3.9 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 +- name: load-balancer-rhel8-v8.0-py3.10-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 v8.0 py3.10 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: "8.0" + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.10/bin/python3 +- name: load-balancer-rhel8-latest-py3.11-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.11 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.11/bin/python3 +- name: load-balancer-rhel8-latest-py3.12-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.12 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.12/bin/python3 +- name: load-balancer-rhel8-latest-py3.13-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 latest py3.13 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: latest + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.13/bin/python3 +- name: load-balancer-rhel8-rapid-pypy3.9-auth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid pypy3.9 Auth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: auth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 +- name: load-balancer-rhel8-rapid-pypy3.10-noauth-ssl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid pypy3.10 NoAuth SSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: noauth + SSL: ssl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 +- name: load-balancer-rhel8-rapid-py3.9-noauth-nossl + tasks: + - name: load-balancer-test + display_name: Load Balancer RHEL8 rapid py3.9 NoAuth NoSSL + run_on: + - rhel87-small + batchtime: 10080 + expansions: + VERSION: rapid + AUTH: noauth + SSL: nossl + test_loadbalancer: "true" + PYTHON_BINARY: /opt/python/3.9/bin/python3 + +- matrix_name: "oidc-auth-test" + matrix_spec: + platform: [ rhel8, macos, windows ] + display_name: "OIDC Auth ${platform}" + tasks: + - name: testoidc_task_group + batchtime: 20160 # 14 days + +- name: testazureoidc-variant + display_name: "OIDC Auth Azure" + run_on: ubuntu2204-small + tasks: + - name: testazureoidc_task_group + batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README + +- name: testgcpoidc-variant + display_name: "OIDC Auth GCP" + run_on: ubuntu2204-small + tasks: + - name: testgcpoidc_task_group + batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README + +- name: testgcpkms-variant + display_name: "GCP KMS" + run_on: + - debian11-small + tasks: + - name: testgcpkms_task_group + batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README + - testgcpkms-fail-task + +- name: testazurekms-variant + display_name: "Azure KMS" + run_on: debian11-small + tasks: + - name: testazurekms_task_group + batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README + - testazurekms-fail-task + +- name: rhel8-test-lambda + display_name: AWS Lambda handler tests run_on: rhel87-small tasks: - name: test_aws_lambda_task_group +- name: rhel8-pr-assign-reviewer + display_name: Assign PR Reviewer + run_on: rhel87-small + tasks: + - name: "assign-pr-reviewer" + - name: rhel8-import-time - display_name: Import Time + display_name: Import Time Check run_on: rhel87-small tasks: - name: "check-import-time" @@ -1811,7 +4374,7 @@ buildvariants: - name: "backport-pr" - name: "perf-tests" - display_name: "Performance Benchmarks" + display_name: "Performance Benchmark Tests" batchtime: 10080 # 7 days run_on: rhel90-dbx-perf-large tasks: diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index 05529ecb25..6d614a9afe 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -9,17 +9,13 @@ # Note: Run this file with `hatch run`, `pipx run`, or `uv run`. from __future__ import annotations -import sys from dataclasses import dataclass -from inspect import getmembers, isfunction from itertools import cycle, product, zip_longest -from pathlib import Path from typing import Any from shrub.v3.evg_build_variant import BuildVariant -from shrub.v3.evg_command import FunctionCall from shrub.v3.evg_project import EvgProject -from shrub.v3.evg_task import EvgTask, EvgTaskRef +from shrub.v3.evg_task import EvgTaskRef from shrub.v3.shrub_service import ShrubService ############## @@ -27,6 +23,7 @@ ############## ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"] +VERSIONS_6_0_PLUS = ["6.0", "7.0", "8.0", "rapid", "latest"] CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] PYPYS = ["pypy3.9", "pypy3.10"] ALL_PYTHONS = CPYTHONS + PYPYS @@ -35,13 +32,7 @@ AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")] TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"] C_EXTS = ["with_ext", "without_ext"] -# By default test each of the topologies with a subset of auth/ssl. -SUB_TASKS = [ - ".sharded_cluster .auth .ssl", - ".replica_set .noauth .ssl", - ".standalone .noauth .nossl", -] -SYNCS = ["sync", "async", "sync_async"] +SYNCS = ["sync", "async"] DISPLAY_LOOKUP = dict( ssl=dict(ssl="SSL", nossl="NoSSL"), auth=dict(auth="Auth", noauth="NoAuth"), @@ -59,15 +50,11 @@ class Host: display_name: str -# Hosts with toolchains. HOSTS["rhel8"] = Host("rhel8", "rhel87-small", "RHEL8") HOSTS["win64"] = Host("win64", "windows-64-vsMulti-small", "Win64") HOSTS["win32"] = Host("win32", "windows-64-vsMulti-small", "Win32") HOSTS["macos"] = Host("macos", "macos-14", "macOS") HOSTS["macos-arm64"] = Host("macos-arm64", "macos-14-arm64", "macOS Arm64") -HOSTS["ubuntu20"] = Host("ubuntu20", "ubuntu2004-small", "Ubuntu-20") -HOSTS["ubuntu22"] = Host("ubuntu22", "ubuntu2204-small", "Ubuntu-22") -HOSTS["rhel7"] = Host("rhel7", "rhel79-small", "RHEL7") ############## @@ -88,12 +75,9 @@ def create_variant( task_refs = [EvgTaskRef(name=n) for n in task_names] kwargs.setdefault("expansions", dict()) expansions = kwargs.pop("expansions", dict()).copy() - if "run_on" in kwargs: - run_on = kwargs.pop("run_on") - else: - host = host or "rhel8" - run_on = [HOSTS[host].run_on] - name = display_name.replace(" ", "-").replace("*-", "").lower() + host = host or "rhel8" + run_on = [HOSTS[host].run_on] + name = display_name.replace(" ", "-").lower() if python: expansions["PYTHON_BINARY"] = get_python_binary(python, host) if version: @@ -119,7 +103,7 @@ def get_python_binary(python: str, host: str) -> str: python = python.replace(".", "") return f"{base}/Python{python}/python.exe" - if host in ["rhel8", "ubuntu22", "ubuntu20", "rhel7"]: + if host == "rhel8": return f"/opt/python/{python}/bin/python3" if host in ["macos", "macos-arm64"]: @@ -128,31 +112,10 @@ def get_python_binary(python: str, host: str) -> str: raise ValueError(f"no match found for python {python} on {host}") -def get_versions_from(min_version: str) -> list[str]: - """Get all server versions starting from a minimum version.""" - min_version_float = float(min_version) - rapid_latest = ["rapid", "latest"] - versions = [v for v in ALL_VERSIONS if v not in rapid_latest] - return [v for v in versions if float(v) >= min_version_float] + rapid_latest - - -def get_versions_until(max_version: str) -> list[str]: - """Get all server version up to a max version.""" - max_version_float = float(max_version) - versions = [v for v in ALL_VERSIONS if v not in ["rapid", "latest"]] - versions = [v for v in versions if float(v) <= max_version_float] - if not len(versions): - raise ValueError(f"No server versions found less <= {max_version}") - return versions - - -def get_display_name(base: str, host: str | None = None, **kwargs) -> str: +def get_display_name(base: str, host: str, **kwargs) -> str: """Get the display name of a variant.""" - display_name = base - if host is not None: - display_name += f" {HOSTS[host].display_name}" + display_name = f"{base} {HOSTS[host].display_name}" version = kwargs.pop("VERSION", None) - version = version or kwargs.pop("version", None) if version: if version not in ["rapid", "latest"]: version = f"v{version}" @@ -201,7 +164,7 @@ def create_ocsp_variants() -> list[BuildVariant]: variants = [] batchtime = BATCHTIME_WEEK * 2 expansions = dict(AUTH="noauth", SSL="ssl", TOPOLOGY="server") - base_display = "OCSP" + base_display = "OCSP test" # OCSP tests on rhel8 with all servers v4.4+ and all python versions. versions = [v for v in ALL_VERSIONS if v != "4.0"] @@ -209,7 +172,7 @@ def create_ocsp_variants() -> list[BuildVariant]: host = "rhel8" variant = create_variant( [".ocsp"], - get_display_name(base_display, host, version=version, python=python), + get_display_name(base_display, host, version, python), python=python, version=version, host=host, @@ -224,7 +187,7 @@ def create_ocsp_variants() -> list[BuildVariant]: python = CPYTHONS[0] if version == "4.4" else CPYTHONS[-1] variant = create_variant( [".ocsp-rsa !.ocsp-staple"], - get_display_name(base_display, host, version=version, python=python), + get_display_name(base_display, host, version, python), python=python, version=version, host=host, @@ -241,13 +204,12 @@ def create_server_variants() -> list[BuildVariant]: # Run the full matrix on linux with min and max CPython, and latest pypy. host = "rhel8" - # Prefix the display name with an asterisk so it is sorted first. - base_display_name = "* Test" - for python in [*MIN_MAX_PYTHON, PYPYS[-1]]: - expansions = dict(COVERAGE="coverage") - display_name = get_display_name(base_display_name, host, python=python, **expansions) + for python, (auth, ssl) in product([*MIN_MAX_PYTHON, PYPYS[-1]], AUTH_SSLS): + display_name = f"Test {host}" + expansions = dict(AUTH=auth, SSL=ssl, COVERAGE="coverage") + display_name = get_display_name("Test", host, python=python, **expansions) variant = create_variant( - [f".{t} .sync_async" for t in TOPOLOGIES], + [f".{t}" for t in TOPOLOGIES], display_name, python=python, host=host, @@ -256,12 +218,15 @@ def create_server_variants() -> list[BuildVariant]: ) variants.append(variant) - # Test the rest of the pythons. - for python in CPYTHONS[1:-1] + PYPYS[:-1]: + # Test the rest of the pythons on linux. + for python, (auth, ssl), topology in zip_cycle( + CPYTHONS[1:-1] + PYPYS[:-1], AUTH_SSLS, TOPOLOGIES + ): display_name = f"Test {host}" - display_name = get_display_name(base_display_name, host, python=python) + expansions = dict(AUTH=auth, SSL=ssl) + display_name = get_display_name("Test", host, python=python, **expansions) variant = create_variant( - [f"{t} .sync_async" for t in SUB_TASKS], + [f".{topology}"], display_name, python=python, host=host, @@ -271,15 +236,16 @@ def create_server_variants() -> list[BuildVariant]: # Test a subset on each of the other platforms. for host in ("macos", "macos-arm64", "win64", "win32"): - for python in MIN_MAX_PYTHON: - tasks = [f"{t} !.sync_async" for t in SUB_TASKS] + for (python, (auth, ssl), topology), sync in product( + zip_cycle(MIN_MAX_PYTHON, AUTH_SSLS, TOPOLOGIES), SYNCS + ): + test_suite = "default" if sync == "sync" else "default_async" + tasks = [f".{topology}"] # MacOS arm64 only works on server versions 6.0+ if host == "macos-arm64": - tasks = [] - for version in get_versions_from("6.0"): - tasks.extend(f"{t} .{version} !.sync_async" for t in SUB_TASKS) - expansions = dict(SKIP_CSOT_TESTS="true") - display_name = get_display_name(base_display_name, host, python=python, **expansions) + tasks = [f".{topology} .{version}" for version in VERSIONS_6_0_PLUS] + expansions = dict(AUTH=auth, SSL=ssl, TEST_SUITES=test_suite, SKIP_CSOT_TESTS="true") + display_name = get_display_name("Test", host, python=python, **expansions) variant = create_variant( tasks, display_name, @@ -297,8 +263,8 @@ def create_encryption_variants() -> list[BuildVariant]: tags = ["encryption_tag"] batchtime = BATCHTIME_WEEK - def get_encryption_expansions(encryption): - expansions = dict(test_encryption="true") + def get_encryption_expansions(encryption, ssl="ssl"): + expansions = dict(AUTH="auth", SSL=ssl, test_encryption="true") if "crypt_shared" in encryption: expansions["test_crypt_shared"] = "true" if "PyOpenSSL" in encryption: @@ -307,13 +273,13 @@ def get_encryption_expansions(encryption): host = "rhel8" - # Test against all server versions for the three main python versions. + # Test against all server versions and topolgies for the three main python versions. encryptions = ["Encryption", "Encryption crypt_shared", "Encryption PyOpenSSL"] for encryption, python in product(encryptions, [*MIN_MAX_PYTHON, PYPYS[-1]]): expansions = get_encryption_expansions(encryption) display_name = get_display_name(encryption, host, python=python, **expansions) variant = create_variant( - [f"{t} .sync_async" for t in SUB_TASKS], + [f".{t}" for t in TOPOLOGIES], display_name, python=python, host=host, @@ -324,11 +290,13 @@ def get_encryption_expansions(encryption): variants.append(variant) # Test the rest of the pythons on linux for all server versions. - for encryption, python, task in zip_cycle(encryptions, CPYTHONS[1:-1] + PYPYS[:-1], SUB_TASKS): - expansions = get_encryption_expansions(encryption) + for encryption, python, ssl in zip_cycle( + encryptions, CPYTHONS[1:-1] + PYPYS[:-1], ["ssl", "nossl"] + ): + expansions = get_encryption_expansions(encryption, ssl) display_name = get_display_name(encryption, host, python=python, **expansions) variant = create_variant( - [f"{task} .sync_async"], + [".replica_set"], display_name, python=python, host=host, @@ -338,9 +306,10 @@ def get_encryption_expansions(encryption): # Test on macos and linux on one server version and topology for min and max python. encryptions = ["Encryption", "Encryption crypt_shared"] - task_names = [".latest .replica_set .sync_async"] + task_names = [".latest .replica_set"] for host, encryption, python in product(["macos", "win64"], encryptions, MIN_MAX_PYTHON): - expansions = get_encryption_expansions(encryption) + ssl = "ssl" if python == CPYTHONS[0] else "nossl" + expansions = get_encryption_expansions(encryption, ssl) display_name = get_display_name(encryption, host, python=python, **expansions) variant = create_variant( task_names, @@ -356,20 +325,25 @@ def get_encryption_expansions(encryption): def create_load_balancer_variants(): - # Load balancer tests - run all supported server versions using the lowest supported python. + # Load balancer tests - run all supported versions for all combinations of auth and ssl and system python. host = "rhel8" + task_names = ["load-balancer-test"] batchtime = BATCHTIME_WEEK - versions = get_versions_from("6.0") + expansions_base = dict(test_loadbalancer="true") + versions = ["6.0", "7.0", "8.0", "latest", "rapid"] variants = [] - for version in versions: - python = CPYTHONS[0] - display_name = get_display_name("Load Balancer", host, python=python, version=version) + pythons = CPYTHONS + PYPYS + for ind, (version, (auth, ssl)) in enumerate(product(versions, AUTH_SSLS)): + expansions = dict(VERSION=version, AUTH=auth, SSL=ssl) + expansions.update(expansions_base) + python = pythons[ind % len(pythons)] + display_name = get_display_name("Load Balancer", host, python=python, **expansions) variant = create_variant( - [".load-balancer"], + task_names, display_name, python=python, host=host, - version=version, + expansions=expansions, batchtime=batchtime, ) variants.append(variant) @@ -380,13 +354,12 @@ def create_compression_variants(): # Compression tests - standalone versions of each server, across python versions, with and without c extensions. # PyPy interpreters are always tested without extensions. host = "rhel8" - base_task = ".standalone .noauth .nossl .sync_async" - task_names = dict(snappy=[base_task], zlib=[base_task], zstd=[f"{base_task} !.4.0"]) + task_names = dict(snappy=[".standalone"], zlib=[".standalone"], zstd=[".standalone !.4.0"]) variants = [] for ind, (compressor, c_ext) in enumerate(product(["snappy", "zlib", "zstd"], C_EXTS)): expansions = dict(COMPRESSORS=compressor) handle_c_ext(c_ext, expansions) - base_name = f"Compression {compressor}" + base_name = f"{compressor} compression" python = CPYTHONS[ind % len(CPYTHONS)] display_name = get_display_name(base_name, host, python=python, **expansions) variant = create_variant( @@ -402,7 +375,7 @@ def create_compression_variants(): for compressor, python in zip_cycle(["snappy", "zlib", "zstd"], other_pythons): expansions = dict(COMPRESSORS=compressor) handle_c_ext(c_ext, expansions) - base_name = f"Compression {compressor}" + base_name = f"{compressor} compression" display_name = get_display_name(base_name, host, python=python, **expansions) variant = create_variant( task_names[compressor], @@ -428,7 +401,7 @@ def create_enterprise_auth_variants(): host = "win64" else: host = "rhel8" - display_name = get_display_name("Auth Enterprise", host, python=python, **expansions) + display_name = get_display_name("Enterprise Auth", host, python=python, **expansions) variant = create_variant( ["test-enterprise-auth"], display_name, host=host, python=python, expansions=expansions ) @@ -440,23 +413,24 @@ def create_enterprise_auth_variants(): def create_pyopenssl_variants(): base_name = "PyOpenSSL" batchtime = BATCHTIME_WEEK - expansions = dict(test_pyopenssl="true") + base_expansions = dict(test_pyopenssl="true", SSL="ssl") variants = [] for python in ALL_PYTHONS: # Only test "noauth" with min python. auth = "noauth" if python == CPYTHONS[0] else "auth" - ssl = "nossl" if auth == "noauth" else "ssl" if python == CPYTHONS[0]: host = "macos" elif python == CPYTHONS[-1]: host = "win64" else: host = "rhel8" + expansions = dict(AUTH=auth) + expansions.update(base_expansions) display_name = get_display_name(base_name, host, python=python) variant = create_variant( - [f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"], + [".replica_set", ".7.0"], display_name, python=python, host=host, @@ -468,397 +442,10 @@ def create_pyopenssl_variants(): return variants -def create_storage_engine_variants(): - host = "rhel8" - engines = ["InMemory", "MMAPv1"] - variants = [] - for engine in engines: - python = CPYTHONS[0] - expansions = dict(STORAGE_ENGINE=engine.lower()) - if engine == engines[0]: - tasks = [f".standalone .noauth .nossl .{v} .sync_async" for v in ALL_VERSIONS] - else: - # MongoDB 4.2 drops support for MMAPv1 - versions = get_versions_until("4.0") - tasks = [f".standalone .{v} .noauth .nossl .sync_async" for v in versions] + [ - f".replica_set .{v} .noauth .nossl .sync_async" for v in versions - ] - display_name = get_display_name(f"Storage {engine}", host, python=python) - variant = create_variant( - tasks, display_name, host=host, python=python, expansions=expansions - ) - variants.append(variant) - return variants - - -def create_stable_api_variants(): - host = "rhel8" - tags = ["versionedApi_tag"] - tasks = [f".standalone .{v} .noauth .nossl .sync_async" for v in get_versions_from("5.0")] - variants = [] - types = ["require v1", "accept v2"] - - # All python versions across platforms. - for python, test_type in product(MIN_MAX_PYTHON, types): - expansions = dict(AUTH="auth") - # Test against a cluster with requireApiVersion=1. - if test_type == types[0]: - # REQUIRE_API_VERSION is set to make drivers-evergreen-tools - # start a cluster with the requireApiVersion parameter. - expansions["REQUIRE_API_VERSION"] = "1" - # MONGODB_API_VERSION is the apiVersion to use in the test suite. - expansions["MONGODB_API_VERSION"] = "1" - else: - # Test against a cluster with acceptApiVersion2 but without - # requireApiVersion, and don't automatically add apiVersion to - # clients created in the test suite. - expansions["ORCHESTRATION_FILE"] = "versioned-api-testing.json" - base_display_name = f"Stable API {test_type}" - display_name = get_display_name(base_display_name, host, python=python, **expansions) - variant = create_variant( - tasks, display_name, host=host, python=python, tags=tags, expansions=expansions - ) - variants.append(variant) - - return variants - - -def create_green_framework_variants(): - variants = [] - tasks = [".standalone .noauth .nossl .sync_async"] - host = "rhel8" - for python, framework in product([CPYTHONS[0], CPYTHONS[-2]], ["eventlet", "gevent"]): - expansions = dict(GREEN_FRAMEWORK=framework, AUTH="auth", SSL="ssl") - display_name = get_display_name(f"Green {framework.capitalize()}", host, python=python) - variant = create_variant( - tasks, display_name, host=host, python=python, expansions=expansions - ) - variants.append(variant) - return variants - - -def create_no_c_ext_variants(): - variants = [] - host = "rhel8" - for python, topology in zip_cycle(CPYTHONS, TOPOLOGIES): - tasks = [f".{topology} .noauth .nossl .sync_async"] - expansions = dict() - handle_c_ext(C_EXTS[0], expansions) - display_name = get_display_name("No C Ext", host, python=python) - variant = create_variant( - tasks, display_name, host=host, python=python, expansions=expansions - ) - variants.append(variant) - return variants - - -def create_atlas_data_lake_variants(): - variants = [] - host = "ubuntu22" - for python, c_ext in product(MIN_MAX_PYTHON, C_EXTS): - tasks = ["atlas-data-lake-tests"] - expansions = dict(AUTH="auth") - handle_c_ext(c_ext, expansions) - display_name = get_display_name("Atlas Data Lake", host, python=python, **expansions) - variant = create_variant( - tasks, display_name, host=host, python=python, expansions=expansions - ) - variants.append(variant) - return variants - - -def create_mod_wsgi_variants(): - variants = [] - host = "ubuntu22" - tasks = [ - "mod-wsgi-standalone", - "mod-wsgi-replica-set", - "mod-wsgi-embedded-mode-standalone", - "mod-wsgi-embedded-mode-replica-set", - ] - expansions = dict(MOD_WSGI_VERSION="4") - for python in MIN_MAX_PYTHON: - display_name = get_display_name("mod_wsgi", host, python=python) - variant = create_variant( - tasks, display_name, host=host, python=python, expansions=expansions - ) - variants.append(variant) - return variants - - -def create_disable_test_commands_variants(): - host = "rhel8" - expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1") - python = CPYTHONS[0] - display_name = get_display_name("Disable test commands", host, python=python) - tasks = [".latest .sync_async"] - return [create_variant(tasks, display_name, host=host, python=python, expansions=expansions)] - - -def create_serverless_variants(): - host = "rhel8" - batchtime = BATCHTIME_WEEK - expansions = dict(test_serverless="true", AUTH="auth", SSL="ssl") - tasks = ["serverless_task_group"] - base_name = "Serverless" - return [ - create_variant( - tasks, - get_display_name(base_name, host, python=python), - host=host, - python=python, - expansions=expansions, - batchtime=batchtime, - ) - for python in MIN_MAX_PYTHON - ] - - -def create_oidc_auth_variants(): - variants = [] - other_tasks = ["testazureoidc_task_group", "testgcpoidc_task_group", "testk8soidc_task_group"] - for host in ["ubuntu22", "macos", "win64"]: - tasks = ["testoidc_task_group"] - if host == "ubuntu22": - tasks += other_tasks - variants.append( - create_variant( - tasks, - get_display_name("Auth OIDC", host), - host=host, - batchtime=BATCHTIME_WEEK * 2, - ) - ) - return variants - - -def create_search_index_variants(): - host = "rhel8" - python = CPYTHONS[0] - return [ - create_variant( - ["test_atlas_task_group_search_indexes"], - get_display_name("Search Index Helpers", host, python=python), - python=python, - host=host, - ) - ] - - -def create_mockupdb_variants(): - host = "rhel8" - python = CPYTHONS[0] - return [ - create_variant( - ["mockupdb"], - get_display_name("MockupDB", host, python=python), - python=python, - host=host, - ) - ] - - -def create_doctests_variants(): - host = "rhel8" - python = CPYTHONS[0] - return [ - create_variant( - ["doctests"], - get_display_name("Doctests", host, python=python), - python=python, - host=host, - ) - ] - - -def create_atlas_connect_variants(): - host = "rhel8" - return [ - create_variant( - ["atlas-connect"], - get_display_name("Atlas connect", host, python=python), - python=python, - host=host, - ) - for python in MIN_MAX_PYTHON - ] - - -def create_aws_auth_variants(): - variants = [] - tasks = [ - "aws-auth-test-4.4", - "aws-auth-test-5.0", - "aws-auth-test-6.0", - "aws-auth-test-7.0", - "aws-auth-test-8.0", - "aws-auth-test-rapid", - "aws-auth-test-latest", - ] - - for host, python in product(["ubuntu20", "win64", "macos"], MIN_MAX_PYTHON): - expansions = dict() - if host != "ubuntu20": - expansions["skip_ECS_auth_test"] = "true" - if host == "macos": - expansions["skip_EC2_auth_test"] = "true" - expansions["skip_web_identity_auth_test"] = "true" - variant = create_variant( - tasks, - get_display_name("Auth AWS", host, python=python), - host=host, - python=python, - expansions=expansions, - ) - variants.append(variant) - return variants - - -def create_alternative_hosts_variants(): - expansions = dict(SKIP_HATCH="true") - batchtime = BATCHTIME_WEEK - variants = [] - - host = "rhel7" - variants.append( - create_variant( - [".5.0 .standalone !.sync_async"], - get_display_name("OpenSSL 1.0.2", "rhel7", python=CPYTHONS[0], **expansions), - host=host, - python=CPYTHONS[0], - batchtime=batchtime, - expansions=expansions, - ) - ) - - hosts = ["rhel92-fips", "rhel8-zseries-small", "rhel8-power-small", "rhel82-arm64-small"] - host_names = ["RHEL9-FIPS", "RHEL8-zseries", "RHEL8-POWER8", "RHEL8-arm64"] - for host, host_name in zip(hosts, host_names): - variants.append( - create_variant( - [".6.0 .standalone !.sync_async"], - display_name=get_display_name(f"Other hosts {host_name}", **expansions), - expansions=expansions, - batchtime=batchtime, - run_on=[host], - ) - ) - return variants - - -############## -# Tasks -############## - - -def create_server_tasks(): - tasks = [] - for topo, version, (auth, ssl), sync in product(TOPOLOGIES, ALL_VERSIONS, AUTH_SSLS, SYNCS): - name = f"test-{version}-{topo}-{auth}-{ssl}-{sync}".lower() - tags = [version, topo, auth, ssl, sync] - bootstrap_vars = dict( - VERSION=version, - TOPOLOGY=topo if topo != "standalone" else "server", - AUTH=auth, - SSL=ssl, - ) - bootstrap_func = FunctionCall(func="bootstrap mongo-orchestration", vars=bootstrap_vars) - test_suites = "" - if sync == "sync": - test_suites = "default" - elif sync == "async": - test_suites = "default_async" - test_vars = dict( - AUTH=auth, - SSL=ssl, - SYNC=sync, - TEST_SUITES=test_suites, - ) - test_func = FunctionCall(func="run tests", vars=test_vars) - tasks.append(EvgTask(name=name, tags=tags, commands=[bootstrap_func, test_func])) - return tasks - - -def create_load_balancer_tasks(): - tasks = [] - for auth, ssl in AUTH_SSLS: - name = f"test-load-balancer-{auth}-{ssl}".lower() - tags = ["load-balancer", auth, ssl] - bootstrap_vars = dict(TOPOLOGY="sharded_cluster", AUTH=auth, SSL=ssl, LOAD_BALANCER="true") - bootstrap_func = FunctionCall(func="bootstrap mongo-orchestration", vars=bootstrap_vars) - balancer_func = FunctionCall(func="run load-balancer") - test_vars = dict(AUTH=auth, SSL=ssl, test_loadbalancer="true") - test_func = FunctionCall(func="run tests", vars=test_vars) - tasks.append( - EvgTask(name=name, tags=tags, commands=[bootstrap_func, balancer_func, test_func]) - ) - return tasks - - ################## # Generate Config ################## - -def write_variants_to_file(): - mod = sys.modules[__name__] - here = Path(__file__).absolute().parent - target = here.parent / "generated_configs" / "variants.yml" - if target.exists(): - target.unlink() - with target.open("w") as fid: - fid.write("buildvariants:\n") - - for name, func in getmembers(mod, isfunction): - if not name.endswith("_variants"): - continue - if not name.startswith("create_"): - raise ValueError("Variant creators must start with create_") - title = name.replace("create_", "").replace("_variants", "").replace("_", " ").capitalize() - project = EvgProject(tasks=None, buildvariants=func()) - out = ShrubService.generate_yaml(project).splitlines() - with target.open("a") as fid: - fid.write(f" # {title} tests\n") - for line in out[1:]: - fid.write(f"{line}\n") - fid.write("\n") - - # Remove extra trailing newline: - data = target.read_text().splitlines() - with target.open("w") as fid: - for line in data[:-1]: - fid.write(f"{line}\n") - - -def write_tasks_to_file(): - mod = sys.modules[__name__] - here = Path(__file__).absolute().parent - target = here.parent / "generated_configs" / "tasks.yml" - if target.exists(): - target.unlink() - with target.open("w") as fid: - fid.write("tasks:\n") - - for name, func in getmembers(mod, isfunction): - if not name.endswith("_tasks"): - continue - if not name.startswith("create_"): - raise ValueError("Task creators must start with create_") - title = name.replace("create_", "").replace("_tasks", "").replace("_", " ").capitalize() - project = EvgProject(tasks=func(), buildvariants=None) - out = ShrubService.generate_yaml(project).splitlines() - with target.open("a") as fid: - fid.write(f" # {title} tests\n") - for line in out[1:]: - fid.write(f"{line}\n") - fid.write("\n") - - # Remove extra trailing newline: - data = target.read_text().splitlines() - with target.open("w") as fid: - for line in data[:-1]: - fid.write(f"{line}\n") - - -write_variants_to_file() -write_tasks_to_file() +variants = create_pyopenssl_variants() +# print(len(variants)) +generate_yaml(variants=variants) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 43517e1fa7..f5369b7c4c 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -32,7 +32,6 @@ """ from __future__ import annotations -import asyncio import contextlib import os import warnings @@ -2037,8 +2036,6 @@ async def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: await self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2053,8 +2050,6 @@ async def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: await self._kill_cursors(cursor_ids, address, topology, session=None) - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2069,8 +2064,6 @@ async def _process_periodic_tasks(self) -> None: try: await self._process_kill_cursors() await self._topology.update_pool() - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index 780704fabb..bbfd6a2998 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -16,7 +16,6 @@ from __future__ import annotations -import asyncio import atexit import logging import time @@ -27,7 +26,7 @@ from pymongo._csot import MovingMinimum from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled from pymongo.hello import Hello -from pymongo.lock import _async_create_lock +from pymongo.lock import _create_lock from pymongo.logger import _SDAM_LOGGER, _debug_log, _SDAMStatusMessage from pymongo.periodic_executor import _shutdown_executors from pymongo.pool_options import _is_faas @@ -277,7 +276,7 @@ async def _check_server(self) -> ServerDescription: await self._reset_connection() if isinstance(error, _OperationCancelled): raise - await self._rtt_monitor.reset() + self._rtt_monitor.reset() # Server type defaults to Unknown. return ServerDescription(address, error=error) @@ -316,9 +315,9 @@ async def _check_once(self) -> ServerDescription: self._cancel_context = conn.cancel_context response, round_trip_time = await self._check_with_socket(conn) if not response.awaitable: - await self._rtt_monitor.add_sample(round_trip_time) + self._rtt_monitor.add_sample(round_trip_time) - avg_rtt, min_rtt = await self._rtt_monitor.get() + avg_rtt, min_rtt = self._rtt_monitor.get() sd = ServerDescription(address, response, avg_rtt, min_round_trip_time=min_rtt) if self._publish: assert self._listeners is not None @@ -414,8 +413,6 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception - except asyncio.CancelledError: - raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -444,7 +441,7 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool self._pool = pool self._moving_average = MovingAverage() self._moving_min = MovingMinimum() - self._lock = _async_create_lock() + self._lock = _create_lock() async def close(self) -> None: self.gc_safe_close() @@ -452,20 +449,20 @@ async def close(self) -> None: # thread has the socket checked out, it will be closed when checked in. await self._pool.reset() - async def add_sample(self, sample: float) -> None: + def add_sample(self, sample: float) -> None: """Add a RTT sample.""" - async with self._lock: + with self._lock: self._moving_average.add_sample(sample) self._moving_min.add_sample(sample) - async def get(self) -> tuple[Optional[float], float]: + def get(self) -> tuple[Optional[float], float]: """Get the calculated average, or None if no samples yet and the min.""" - async with self._lock: + with self._lock: return self._moving_average.get(), self._moving_min.get() - async def reset(self) -> None: + def reset(self) -> None: """Reset the average RTT.""" - async with self._lock: + with self._lock: self._moving_average.reset() self._moving_min.reset() @@ -475,12 +472,10 @@ async def _run(self) -> None: # heartbeat protocol (MongoDB 4.4+). # XXX: Skip check if the server is unknown? rtt = await self._ping() - await self.add_sample(rtt) + self.add_sample(rtt) except ReferenceError: # Topology was garbage-collected. await self.close() - except asyncio.CancelledError: - raise except Exception: await self._pool.reset() diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index a37aa3b46a..2fe9579aef 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -704,8 +704,6 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() - except asyncio.CancelledError: - raise except Exception: # noqa: S110 pass diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index 377689047b..aa16e85a07 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -271,8 +271,7 @@ async def async_receive_data( ) for task in pending: task.cancel() - if pending: - await asyncio.wait(pending) + await asyncio.wait(pending) if len(done) == 0: raise socket.timeout("timed out") if read_task in done: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 055688ab65..badcffe081 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -32,7 +32,6 @@ """ from __future__ import annotations -import asyncio import contextlib import os import warnings @@ -2031,8 +2030,6 @@ def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2047,8 +2044,6 @@ def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: self._kill_cursors(cursor_ids, address, topology, session=None) - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2063,8 +2058,6 @@ def _process_periodic_tasks(self) -> None: try: self._process_kill_cursors() self._topology.update_pool() - except asyncio.CancelledError: - raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index d82a5f976d..a806670f2c 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -16,7 +16,6 @@ from __future__ import annotations -import asyncio import atexit import logging import time @@ -414,8 +413,6 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception - except asyncio.CancelledError: - raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -479,8 +476,6 @@ def _run(self) -> None: except ReferenceError: # Topology was garbage-collected. self.close() - except asyncio.CancelledError: - raise except Exception: self._pool.reset() diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 99201b822e..6ac7b4eca9 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -702,8 +702,6 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() - except asyncio.CancelledError: - raise except Exception: # noqa: S110 pass diff --git a/test/__init__.py b/test/__init__.py index dba3312424..c1944f5870 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -17,7 +17,6 @@ import asyncio import gc -import logging import multiprocessing import os import signal @@ -26,7 +25,6 @@ import sys import threading import time -import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -193,8 +191,6 @@ def _connect(self, host, port, **kwargs): client.close() def _init_client(self): - self.mongoses = [] - self.connection_attempts = [] self.client = self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index bed49de161..9ca5a32ffc 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -17,7 +17,6 @@ import asyncio import gc -import logging import multiprocessing import os import signal @@ -26,7 +25,6 @@ import sys import threading import time -import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -193,8 +191,6 @@ async def _connect(self, host, port, **kwargs): await client.close() async def _init_client(self): - self.mongoses = [] - self.connection_attempts = [] self.client = await self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/test_auth_spec.py b/test/asynchronous/test_auth_spec.py index e9e43d5759..a6ab1cb331 100644 --- a/test/asynchronous/test_auth_spec.py +++ b/test/asynchronous/test_auth_spec.py @@ -25,7 +25,7 @@ sys.path[0:0] = [""] from test import unittest -from test.asynchronous.unified_format import generate_test_classes +from test.unified_format import generate_test_classes from pymongo import AsyncMongoClient from pymongo.asynchronous.auth_oidc import OIDCCallback diff --git a/test/asynchronous/test_change_stream.py b/test/asynchronous/test_change_stream.py index 08da00cc1e..873631bbe5 100644 --- a/test/asynchronous/test_change_stream.py +++ b/test/asynchronous/test_change_stream.py @@ -35,7 +35,7 @@ async_client_context, unittest, ) -from test.asynchronous.unified_format import generate_test_classes +from test.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, EventListener, diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 292a78d645..47cbff6d5b 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -2580,7 +2580,7 @@ async def test_direct_client_maintains_pool_to_arbiter(self): await async_wait_until(lambda: len(c.nodes) == 1, "connect") self.assertEqual(await c.address, ("c", 3)) # Assert that we create 1 pooled connection. - await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 1) + listener.wait_for_event(monitoring.ConnectionReadyEvent, 1) self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1) arbiter = c._topology.get_server_by_address(("c", 3)) self.assertEqual(len(arbiter.pool.conns), 1) diff --git a/test/asynchronous/test_connection_logging.py b/test/asynchronous/test_connection_logging.py index 945c6c59b5..6bc9835b70 100644 --- a/test/asynchronous/test_connection_logging.py +++ b/test/asynchronous/test_connection_logging.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.asynchronous.unified_format import generate_test_classes +from test.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py index bc9638b443..ffff428379 100644 --- a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py +++ b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py @@ -44,6 +44,9 @@ class TestAsyncConnectionsSurvivePrimaryStepDown(AsyncIntegrationTest): listener: CMAPListener coll: AsyncCollection + async def asyncTearDown(self): + await reset_client_context() + @async_client_context.require_replica_set async def asyncSetUp(self): self.listener = CMAPListener() diff --git a/test/asynchronous/test_create_entities.py b/test/asynchronous/test_create_entities.py index 1f68cf6ddc..cb2ec63f4c 100644 --- a/test/asynchronous/test_create_entities.py +++ b/test/asynchronous/test_create_entities.py @@ -56,9 +56,6 @@ async def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) - if self.scenario_runner.mongos_clients: - for client in self.scenario_runner.mongos_clients: - await client.close() async def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -125,9 +122,6 @@ async def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) - if self.scenario_runner.mongos_clients: - for client in self.scenario_runner.mongos_clients: - await client.close() if __name__ == "__main__": diff --git a/test/asynchronous/test_crud_unified.py b/test/asynchronous/test_crud_unified.py index e6f42d5bdf..3d8deb36e9 100644 --- a/test/asynchronous/test_crud_unified.py +++ b/test/asynchronous/test_crud_unified.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.asynchronous.unified_format import generate_test_classes +from test.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index a34741c144..9c88f6ff20 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -46,7 +46,6 @@ unittest, ) from test.asynchronous.test_bulk import AsyncBulkTestBase -from test.asynchronous.unified_format import generate_test_classes from test.asynchronous.utils_spec_runner import AsyncSpecRunner from test.helpers import ( AWS_CREDS, @@ -57,6 +56,7 @@ KMIP_CREDS, LOCAL_MASTER_KEY, ) +from test.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, OvertCommandListener, diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 9018452b54..189e69a921 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -304,6 +304,7 @@ async def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = await self.test.async_rs_or_single_client(**kwargs) self[spec["id"]] = client + self.test.addAsyncCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -529,6 +530,11 @@ async def asyncSetUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) + async def asyncTearDown(self): + for client in self.mongos_clients: + await client.close() + await super().asyncTearDown() + def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if async_client_context.storage_engine == "mmapv1": @@ -1036,6 +1042,7 @@ async def _testOperation_targetedFailPoint(self, spec): ) client = await self.async_single_client("{}:{}".format(*session._pinned_address)) + self.addAsyncCleanup(client.close) await self.__set_fail_point(client=client, command_args=spec["failPoint"]) async def _testOperation_createEntities(self, spec): diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index 75aa50b578..f0463244d7 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -264,6 +264,8 @@ async def asyncSetUp(self) -> None: async def asyncTearDown(self) -> None: self.knobs.disable() + for client in self.mongos_clients: + await client.close() async def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) diff --git a/test/test_connections_survive_primary_stepdown_spec.py b/test/test_connections_survive_primary_stepdown_spec.py index 84ef6decd5..4387850a00 100644 --- a/test/test_connections_survive_primary_stepdown_spec.py +++ b/test/test_connections_survive_primary_stepdown_spec.py @@ -44,6 +44,9 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest): listener: CMAPListener coll: Collection + def tearDown(self): + reset_client_context() + @client_context.require_replica_set def setUp(self): self.listener = CMAPListener() diff --git a/test/test_create_entities.py b/test/test_create_entities.py index 9d77a08eee..ad75fe5702 100644 --- a/test/test_create_entities.py +++ b/test/test_create_entities.py @@ -56,9 +56,6 @@ def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) - if self.scenario_runner.mongos_clients: - for client in self.scenario_runner.mongos_clients: - client.close() def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -125,9 +122,6 @@ def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) - if self.scenario_runner.mongos_clients: - for client in self.scenario_runner.mongos_clients: - client.close() if __name__ == "__main__": diff --git a/test/unified_format.py b/test/unified_format.py index 8e62ef8e1d..766489fb7c 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -303,6 +303,7 @@ def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = self.test.rs_or_single_client(**kwargs) self[spec["id"]] = client + self.test.addCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -528,6 +529,11 @@ def setUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) + def tearDown(self): + for client in self.mongos_clients: + client.close() + super().tearDown() + def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if client_context.storage_engine == "mmapv1": @@ -1027,6 +1033,7 @@ def _testOperation_targetedFailPoint(self, spec): ) client = self.single_client("{}:{}".format(*session._pinned_address)) + self.addCleanup(client.close) self.__set_fail_point(client=client, command_args=spec["failPoint"]) def _testOperation_createEntities(self, spec): diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index 3dea4ede1c..682cf0b0f8 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -264,6 +264,8 @@ def setUp(self) -> None: def tearDown(self) -> None: self.knobs.disable() + for client in self.mongos_clients: + client.close() def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) From f6e35d9d8fd534e7183fff52d23fa6c911660f18 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 12 Nov 2024 09:29:59 -0500 Subject: [PATCH 10/14] PYTHON-4945 - Fix test cleanups for mongoses, use asyncio locks, propagate CancelledErrors (#1996) --- pymongo/asynchronous/mongo_client.py | 7 +++++ pymongo/asynchronous/monitor.py | 29 +++++++++++-------- pymongo/asynchronous/pool.py | 2 ++ pymongo/network_layer.py | 3 +- pymongo/synchronous/mongo_client.py | 7 +++++ pymongo/synchronous/monitor.py | 5 ++++ pymongo/synchronous/pool.py | 2 ++ test/__init__.py | 4 +++ test/asynchronous/__init__.py | 4 +++ test/asynchronous/test_auth_spec.py | 2 +- test/asynchronous/test_change_stream.py | 2 +- test/asynchronous/test_client.py | 2 +- test/asynchronous/test_connection_logging.py | 2 +- ...nnections_survive_primary_stepdown_spec.py | 3 -- test/asynchronous/test_create_entities.py | 6 ++++ test/asynchronous/test_crud_unified.py | 2 +- test/asynchronous/test_encryption.py | 2 +- test/asynchronous/unified_format.py | 11 +++---- test/asynchronous/utils_spec_runner.py | 2 -- ...nnections_survive_primary_stepdown_spec.py | 3 -- test/test_create_entities.py | 6 ++++ test/unified_format.py | 11 +++---- test/utils_spec_runner.py | 2 -- 23 files changed, 76 insertions(+), 43 deletions(-) diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index f5369b7c4c..43517e1fa7 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -32,6 +32,7 @@ """ from __future__ import annotations +import asyncio import contextlib import os import warnings @@ -2036,6 +2037,8 @@ async def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: await self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2050,6 +2053,8 @@ async def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: await self._kill_cursors(cursor_ids, address, topology, session=None) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2064,6 +2069,8 @@ async def _process_periodic_tasks(self) -> None: try: await self._process_kill_cursors() await self._topology.update_pool() + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index bbfd6a2998..780704fabb 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -16,6 +16,7 @@ from __future__ import annotations +import asyncio import atexit import logging import time @@ -26,7 +27,7 @@ from pymongo._csot import MovingMinimum from pymongo.errors import NetworkTimeout, NotPrimaryError, OperationFailure, _OperationCancelled from pymongo.hello import Hello -from pymongo.lock import _create_lock +from pymongo.lock import _async_create_lock from pymongo.logger import _SDAM_LOGGER, _debug_log, _SDAMStatusMessage from pymongo.periodic_executor import _shutdown_executors from pymongo.pool_options import _is_faas @@ -276,7 +277,7 @@ async def _check_server(self) -> ServerDescription: await self._reset_connection() if isinstance(error, _OperationCancelled): raise - self._rtt_monitor.reset() + await self._rtt_monitor.reset() # Server type defaults to Unknown. return ServerDescription(address, error=error) @@ -315,9 +316,9 @@ async def _check_once(self) -> ServerDescription: self._cancel_context = conn.cancel_context response, round_trip_time = await self._check_with_socket(conn) if not response.awaitable: - self._rtt_monitor.add_sample(round_trip_time) + await self._rtt_monitor.add_sample(round_trip_time) - avg_rtt, min_rtt = self._rtt_monitor.get() + avg_rtt, min_rtt = await self._rtt_monitor.get() sd = ServerDescription(address, response, avg_rtt, min_round_trip_time=min_rtt) if self._publish: assert self._listeners is not None @@ -413,6 +414,8 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception + except asyncio.CancelledError: + raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -441,7 +444,7 @@ def __init__(self, topology: Topology, topology_settings: TopologySettings, pool self._pool = pool self._moving_average = MovingAverage() self._moving_min = MovingMinimum() - self._lock = _create_lock() + self._lock = _async_create_lock() async def close(self) -> None: self.gc_safe_close() @@ -449,20 +452,20 @@ async def close(self) -> None: # thread has the socket checked out, it will be closed when checked in. await self._pool.reset() - def add_sample(self, sample: float) -> None: + async def add_sample(self, sample: float) -> None: """Add a RTT sample.""" - with self._lock: + async with self._lock: self._moving_average.add_sample(sample) self._moving_min.add_sample(sample) - def get(self) -> tuple[Optional[float], float]: + async def get(self) -> tuple[Optional[float], float]: """Get the calculated average, or None if no samples yet and the min.""" - with self._lock: + async with self._lock: return self._moving_average.get(), self._moving_min.get() - def reset(self) -> None: + async def reset(self) -> None: """Reset the average RTT.""" - with self._lock: + async with self._lock: self._moving_average.reset() self._moving_min.reset() @@ -472,10 +475,12 @@ async def _run(self) -> None: # heartbeat protocol (MongoDB 4.4+). # XXX: Skip check if the server is unknown? rtt = await self._ping() - self.add_sample(rtt) + await self.add_sample(rtt) except ReferenceError: # Topology was garbage-collected. await self.close() + except asyncio.CancelledError: + raise except Exception: await self._pool.reset() diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index 2fe9579aef..a37aa3b46a 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -704,6 +704,8 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() + except asyncio.CancelledError: + raise except Exception: # noqa: S110 pass diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index aa16e85a07..377689047b 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -271,7 +271,8 @@ async def async_receive_data( ) for task in pending: task.cancel() - await asyncio.wait(pending) + if pending: + await asyncio.wait(pending) if len(done) == 0: raise socket.timeout("timed out") if read_task in done: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index badcffe081..055688ab65 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -32,6 +32,7 @@ """ from __future__ import annotations +import asyncio import contextlib import os import warnings @@ -2030,6 +2031,8 @@ def _process_kill_cursors(self) -> None: for address, cursor_id, conn_mgr in pinned_cursors: try: self._cleanup_cursor_lock(cursor_id, address, conn_mgr, None, False) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: # Raise the exception when client is closed so that it @@ -2044,6 +2047,8 @@ def _process_kill_cursors(self) -> None: for address, cursor_ids in address_to_cursor_ids.items(): try: self._kill_cursors(cursor_ids, address, topology, session=None) + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: raise @@ -2058,6 +2063,8 @@ def _process_periodic_tasks(self) -> None: try: self._process_kill_cursors() self._topology.update_pool() + except asyncio.CancelledError: + raise except Exception as exc: if isinstance(exc, InvalidOperation) and self._topology._closed: return diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index a806670f2c..d82a5f976d 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -16,6 +16,7 @@ from __future__ import annotations +import asyncio import atexit import logging import time @@ -413,6 +414,8 @@ def _get_seedlist(self) -> Optional[list[tuple[str, Any]]]: if len(seedlist) == 0: # As per the spec: this should be treated as a failure. raise Exception + except asyncio.CancelledError: + raise except Exception: # As per the spec, upon encountering an error: # - An error must not be raised @@ -476,6 +479,8 @@ def _run(self) -> None: except ReferenceError: # Topology was garbage-collected. self.close() + except asyncio.CancelledError: + raise except Exception: self._pool.reset() diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 6ac7b4eca9..99201b822e 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -702,6 +702,8 @@ def _close_conn(self) -> None: # shutdown. try: self.conn.close() + except asyncio.CancelledError: + raise except Exception: # noqa: S110 pass diff --git a/test/__init__.py b/test/__init__.py index c1944f5870..dba3312424 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -17,6 +17,7 @@ import asyncio import gc +import logging import multiprocessing import os import signal @@ -25,6 +26,7 @@ import sys import threading import time +import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -191,6 +193,8 @@ def _connect(self, host, port, **kwargs): client.close() def _init_client(self): + self.mongoses = [] + self.connection_attempts = [] self.client = self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index 9ca5a32ffc..bed49de161 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -17,6 +17,7 @@ import asyncio import gc +import logging import multiprocessing import os import signal @@ -25,6 +26,7 @@ import sys import threading import time +import traceback import unittest import warnings from asyncio import iscoroutinefunction @@ -191,6 +193,8 @@ async def _connect(self, host, port, **kwargs): await client.close() async def _init_client(self): + self.mongoses = [] + self.connection_attempts = [] self.client = await self._connect(host, port) if self.client is not None: # Return early when connected to dataLake as mongohoused does not diff --git a/test/asynchronous/test_auth_spec.py b/test/asynchronous/test_auth_spec.py index a6ab1cb331..e9e43d5759 100644 --- a/test/asynchronous/test_auth_spec.py +++ b/test/asynchronous/test_auth_spec.py @@ -25,7 +25,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes from pymongo import AsyncMongoClient from pymongo.asynchronous.auth_oidc import OIDCCallback diff --git a/test/asynchronous/test_change_stream.py b/test/asynchronous/test_change_stream.py index 873631bbe5..08da00cc1e 100644 --- a/test/asynchronous/test_change_stream.py +++ b/test/asynchronous/test_change_stream.py @@ -35,7 +35,7 @@ async_client_context, unittest, ) -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, EventListener, diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 47cbff6d5b..292a78d645 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -2580,7 +2580,7 @@ async def test_direct_client_maintains_pool_to_arbiter(self): await async_wait_until(lambda: len(c.nodes) == 1, "connect") self.assertEqual(await c.address, ("c", 3)) # Assert that we create 1 pooled connection. - listener.wait_for_event(monitoring.ConnectionReadyEvent, 1) + await listener.async_wait_for_event(monitoring.ConnectionReadyEvent, 1) self.assertEqual(listener.event_count(monitoring.ConnectionCreatedEvent), 1) arbiter = c._topology.get_server_by_address(("c", 3)) self.assertEqual(len(arbiter.pool.conns), 1) diff --git a/test/asynchronous/test_connection_logging.py b/test/asynchronous/test_connection_logging.py index 6bc9835b70..945c6c59b5 100644 --- a/test/asynchronous/test_connection_logging.py +++ b/test/asynchronous/test_connection_logging.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py index ffff428379..bc9638b443 100644 --- a/test/asynchronous/test_connections_survive_primary_stepdown_spec.py +++ b/test/asynchronous/test_connections_survive_primary_stepdown_spec.py @@ -44,9 +44,6 @@ class TestAsyncConnectionsSurvivePrimaryStepDown(AsyncIntegrationTest): listener: CMAPListener coll: AsyncCollection - async def asyncTearDown(self): - await reset_client_context() - @async_client_context.require_replica_set async def asyncSetUp(self): self.listener = CMAPListener() diff --git a/test/asynchronous/test_create_entities.py b/test/asynchronous/test_create_entities.py index cb2ec63f4c..1f68cf6ddc 100644 --- a/test/asynchronous/test_create_entities.py +++ b/test/asynchronous/test_create_entities.py @@ -56,6 +56,9 @@ async def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + await client.close() async def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -122,6 +125,9 @@ async def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + await client.close() if __name__ == "__main__": diff --git a/test/asynchronous/test_crud_unified.py b/test/asynchronous/test_crud_unified.py index 3d8deb36e9..e6f42d5bdf 100644 --- a/test/asynchronous/test_crud_unified.py +++ b/test/asynchronous/test_crud_unified.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test import unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes _IS_SYNC = False diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index 9c88f6ff20..a34741c144 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -46,6 +46,7 @@ unittest, ) from test.asynchronous.test_bulk import AsyncBulkTestBase +from test.asynchronous.unified_format import generate_test_classes from test.asynchronous.utils_spec_runner import AsyncSpecRunner from test.helpers import ( AWS_CREDS, @@ -56,7 +57,6 @@ KMIP_CREDS, LOCAL_MASTER_KEY, ) -from test.unified_format import generate_test_classes from test.utils import ( AllowListEventListener, OvertCommandListener, diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 189e69a921..b2c889c461 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -304,7 +304,6 @@ async def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = await self.test.async_rs_or_single_client(**kwargs) self[spec["id"]] = client - self.test.addAsyncCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -499,6 +498,10 @@ async def asyncSetUp(self): # process file-level runOnRequirements run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) if not await self.should_run_on(run_on_spec): + # Explicitly close async clients here + # to prevent leaky monitor tasks + if not _IS_SYNC: + await async_client_context.client.close() raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here @@ -530,11 +533,6 @@ async def asyncSetUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) - async def asyncTearDown(self): - for client in self.mongos_clients: - await client.close() - await super().asyncTearDown() - def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if async_client_context.storage_engine == "mmapv1": @@ -1042,7 +1040,6 @@ async def _testOperation_targetedFailPoint(self, spec): ) client = await self.async_single_client("{}:{}".format(*session._pinned_address)) - self.addAsyncCleanup(client.close) await self.__set_fail_point(client=client, command_args=spec["failPoint"]) async def _testOperation_createEntities(self, spec): diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index f0463244d7..75aa50b578 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -264,8 +264,6 @@ async def asyncSetUp(self) -> None: async def asyncTearDown(self) -> None: self.knobs.disable() - for client in self.mongos_clients: - await client.close() async def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) diff --git a/test/test_connections_survive_primary_stepdown_spec.py b/test/test_connections_survive_primary_stepdown_spec.py index 4387850a00..84ef6decd5 100644 --- a/test/test_connections_survive_primary_stepdown_spec.py +++ b/test/test_connections_survive_primary_stepdown_spec.py @@ -44,9 +44,6 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest): listener: CMAPListener coll: Collection - def tearDown(self): - reset_client_context() - @client_context.require_replica_set def setUp(self): self.listener = CMAPListener() diff --git a/test/test_create_entities.py b/test/test_create_entities.py index ad75fe5702..9d77a08eee 100644 --- a/test/test_create_entities.py +++ b/test/test_create_entities.py @@ -56,6 +56,9 @@ def test_store_events_as_entities(self): self.assertGreater(len(final_entity_map["events1"]), 0) for event in final_entity_map["events1"]: self.assertIn("PoolCreatedEvent", event["name"]) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + client.close() def test_store_all_others_as_entities(self): self.scenario_runner = UnifiedSpecTestMixinV1() @@ -122,6 +125,9 @@ def test_store_all_others_as_entities(self): self.assertEqual(entity_map["failures"], []) self.assertEqual(entity_map["successes"], 2) self.assertEqual(entity_map["iterations"], 5) + if self.scenario_runner.mongos_clients: + for client in self.scenario_runner.mongos_clients: + client.close() if __name__ == "__main__": diff --git a/test/unified_format.py b/test/unified_format.py index 766489fb7c..7a38bd4b5e 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -303,7 +303,6 @@ def _create_entity(self, entity_spec, uri=None): kwargs["h"] = uri client = self.test.rs_or_single_client(**kwargs) self[spec["id"]] = client - self.test.addCleanup(client.close) return elif entity_type == "database": client = self[spec["client"]] @@ -498,6 +497,10 @@ def setUp(self): # process file-level runOnRequirements run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) if not self.should_run_on(run_on_spec): + # Explicitly close async clients here + # to prevent leaky monitor tasks + if not _IS_SYNC: + client_context.client.close() raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here @@ -529,11 +532,6 @@ def setUp(self): # initialize internals self.match_evaluator = MatchEvaluatorUtil(self) - def tearDown(self): - for client in self.mongos_clients: - client.close() - super().tearDown() - def maybe_skip_test(self, spec): # add any special-casing for skipping tests here if client_context.storage_engine == "mmapv1": @@ -1033,7 +1031,6 @@ def _testOperation_targetedFailPoint(self, spec): ) client = self.single_client("{}:{}".format(*session._pinned_address)) - self.addCleanup(client.close) self.__set_fail_point(client=client, command_args=spec["failPoint"]) def _testOperation_createEntities(self, spec): diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index 682cf0b0f8..3dea4ede1c 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -264,8 +264,6 @@ def setUp(self) -> None: def tearDown(self) -> None: self.knobs.disable() - for client in self.mongos_clients: - client.close() def _set_fail_point(self, client, command_args): cmd = SON([("configureFailPoint", "failCommand")]) From ce518643772ec532ee7c466d335211163d8d0ef1 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 12 Nov 2024 11:45:39 -0500 Subject: [PATCH 11/14] Fix EVG changes for async-improvements (#2002) --- .evergreen/config.yml | 2731 +------------------------ .evergreen/scripts/generate_config.py | 142 +- 2 files changed, 186 insertions(+), 2687 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index e357f02f2b..fc1713a88e 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -25,6 +25,10 @@ timeout: script: | ls -la +include: + - filename: .evergreen/generated_configs/tasks.yml + - filename: .evergreen/generated_configs/variants.yml + functions: "fetch source": # Executes clone and applies the submitted patch, if any @@ -516,6 +520,18 @@ functions: args: - .evergreen/run-mongodb-oidc-test.sh + "run oidc k8s auth test": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: src + env: + OIDC_ENV: k8s + include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "K8S_VARIANT"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh + "run aws auth test with aws credentials as environment variables": - command: shell.exec type: test @@ -869,6 +885,32 @@ task_groups: tasks: - oidc-auth-test-gcp + - name: testk8soidc_task_group + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - func: make files executable + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + duration_seconds: 1800 + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-k8s + - name: testoidc_task_group setup_group: - func: fetch source @@ -968,249 +1010,6 @@ tasks: TOPOLOGY: "server" - func: "run doctests" - - name: "test-4.0-standalone" - tags: ["4.0", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.0" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-4.0-replica_set" - tags: ["4.0", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.0" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-4.0-sharded_cluster" - tags: ["4.0", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.0" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-4.2-standalone" - tags: ["4.2", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.2" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-4.2-replica_set" - tags: ["4.2", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.2" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-4.2-sharded_cluster" - tags: ["4.2", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.2" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-4.4-standalone" - tags: ["4.4", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.4" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-4.4-replica_set" - tags: ["4.4", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.4" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-4.4-sharded_cluster" - tags: ["4.4", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "4.4" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-5.0-standalone" - tags: ["5.0", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "5.0" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-5.0-replica_set" - tags: ["5.0", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "5.0" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-5.0-sharded_cluster" - tags: ["5.0", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "5.0" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-6.0-standalone" - tags: ["6.0", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "6.0" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-6.0-replica_set" - tags: ["6.0", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "6.0" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-6.0-sharded_cluster" - tags: ["6.0", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "6.0" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-8.0-standalone" - tags: ["8.0", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "8.0" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-8.0-replica_set" - tags: ["8.0", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "8.0" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-8.0-sharded_cluster" - tags: ["8.0", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "8.0" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-7.0-standalone" - tags: ["7.0", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "7.0" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-7.0-replica_set" - tags: ["7.0", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "7.0" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-7.0-sharded_cluster" - tags: ["7.0", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "7.0" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-latest-standalone" - tags: ["latest", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "latest" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-latest-replica_set" - tags: ["latest", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "latest" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-latest-sharded_cluster" - tags: ["latest", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "latest" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - - name: "test-rapid-standalone" - tags: ["rapid", "standalone"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "rapid" - TOPOLOGY: "server" - - func: "run tests" - - - name: "test-rapid-replica_set" - tags: ["rapid", "replica_set"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "rapid" - TOPOLOGY: "replica_set" - - func: "run tests" - - - name: "test-rapid-sharded_cluster" - tags: ["rapid", "sharded_cluster"] - commands: - - func: "bootstrap mongo-orchestration" - vars: - VERSION: "rapid" - TOPOLOGY: "sharded_cluster" - - func: "run tests" - - name: "test-serverless" tags: ["serverless"] commands: @@ -1781,64 +1580,47 @@ tasks: - func: "run aws auth test with aws web identity credentials" - func: "run aws ECS auth test" - - name: load-balancer-test - commands: - - func: "bootstrap mongo-orchestration" - vars: - TOPOLOGY: "sharded_cluster" - LOAD_BALANCER: true - - func: "run load-balancer" - - func: "run tests" - - name: "oidc-auth-test" commands: - func: "run oidc auth test with test credentials" - name: "oidc-auth-test-azure" commands: - - command: shell.exec + - command: subprocess.exec type: test params: - shell: bash - script: |- - set -o errexit - . src/.evergreen/scripts/env.sh - cd src - git add . - git commit -m "add files" - export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz - git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD - export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + binary: bash + working_dir: src + env: + OIDC_ENV: azure + include_expansions_in_env: ["DRIVERS_TOOLS"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh - name: "oidc-auth-test-gcp" commands: - - command: shell.exec + - command: subprocess.exec type: test params: - shell: bash - script: |- - set -o errexit - . src/.evergreen/scripts/env.sh - cd src - git add . - git commit -m "add files" - export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz - git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD - # Define the command to run on the VM. - # Ensure that we source the environment file created for us, set up any other variables we need, - # and then run our test suite on the vm. - export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh - - - name: "test-fips-standalone" - tags: ["fips"] + binary: bash + working_dir: src + env: + OIDC_ENV: gcp + include_expansions_in_env: ["DRIVERS_TOOLS"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh + + - name: "oidc-auth-test-k8s" commands: - - func: "bootstrap mongo-orchestration" + - func: "run oidc k8s auth test" vars: - VERSION: "latest" - TOPOLOGY: "server" - - func: "run tests" + K8S_VARIANT: eks + - func: "run oidc k8s auth test" + vars: + K8S_VARIANT: gke + - func: "run oidc k8s auth test" + vars: + K8S_VARIANT: aks # }}} - name: "coverage-report" tags: ["coverage"] @@ -1954,23 +1736,6 @@ tasks: - func: "attach benchmark test results" - func: "send dashboard data" - - name: "assign-pr-reviewer" - tags: ["pr"] - allowed_requesters: ["patch", "github_pr"] - commands: - - command: shell.exec - type: test - params: - shell: "bash" - working_dir: src - script: | - . .evergreen/scripts/env.sh - set -x - export CONFIG=$PROJECT_DIRECTORY/.github/reviewers.txt - export SCRIPT="$DRIVERS_TOOLS/.evergreen/github_app/assign-reviewer.sh" - bash $SCRIPT -p $CONFIG -h ${github_commit} -o "mongodb" -n "mongo-python-driver" - echo '{"results": [{ "status": "PASS", "test_file": "Build", "log_raw": "Test completed" } ]}' > ${PROJECT_DIRECTORY}/test-results.json - - name: "check-import-time" tags: ["pr"] commands: @@ -1999,2369 +1764,41 @@ tasks: - mongo-python-driver - ${github_commit} -axes: - # Choice of distro - - id: platform - display_name: OS - values: - - id: macos - display_name: "macOS" - run_on: macos-14 - variables: - skip_EC2_auth_test: true - skip_ECS_auth_test: true - skip_web_identity_auth_test: true - # CSOT tests are unreliable on our slow macOS hosts. - SKIP_CSOT_TESTS: true - - id: macos-arm64 - display_name: "macOS Arm64" - run_on: macos-14-arm64 - variables: - skip_EC2_auth_test: true - skip_ECS_auth_test: true - skip_web_identity_auth_test: true - # CSOT tests are unreliable on our slow macOS hosts. - SKIP_CSOT_TESTS: true - - id: rhel7 - display_name: "RHEL 7.x" - run_on: rhel79-small - batchtime: 10080 # 7 days - - id: rhel8 - display_name: "RHEL 8.x" - run_on: rhel8.8-small - batchtime: 10080 # 7 days - - id: rhel9-fips - display_name: "RHEL 9 FIPS" - run_on: rhel92-fips - batchtime: 10080 # 7 days - - id: ubuntu-22.04 - display_name: "Ubuntu 22.04" - run_on: ubuntu2204-small - batchtime: 10080 # 7 days - - id: ubuntu-20.04 - display_name: "Ubuntu 20.04" - run_on: ubuntu2004-small - batchtime: 10080 # 7 days - - id: rhel8-zseries - display_name: "RHEL 8 (zSeries)" - run_on: rhel8-zseries-small - batchtime: 10080 # 7 days - variables: - SKIP_HATCH: true - - id: rhel8-power8 - display_name: "RHEL 8 (POWER8)" - run_on: rhel8-power-small - batchtime: 10080 # 7 days - variables: - SKIP_HATCH: true - - id: rhel8-arm64 - display_name: "RHEL 8 (ARM64)" - run_on: rhel82-arm64-small - batchtime: 10080 # 7 days - variables: - - id: windows - display_name: "Windows 64" - run_on: windows-64-vsMulti-small - batchtime: 10080 # 7 days - variables: - skip_ECS_auth_test: true - skip_EC2_auth_test: true - skip_web_identity_auth_test: true - venv_bin_dir: "Scripts" - # CSOT tests are unreliable on our slow Windows hosts. - SKIP_CSOT_TESTS: true - - # Test with authentication? - - id: auth - display_name: Authentication - values: - - id: auth - display_name: Auth - variables: - AUTH: "auth" - - id: noauth - display_name: NoAuth - variables: - AUTH: "noauth" - - # Test with SSL? - - id: ssl - display_name: SSL - values: - - id: ssl - display_name: SSL - variables: - SSL: "ssl" - - id: nossl - display_name: NoSSL - variables: - SSL: "nossl" - - # Test with Auth + SSL (combined for convenience)? - - id: auth-ssl - display_name: Auth SSL - values: - - id: auth-ssl - display_name: Auth SSL - variables: - AUTH: "auth" - SSL: "ssl" - - id: noauth-nossl - display_name: NoAuth NoSSL - variables: - AUTH: "noauth" - SSL: "nossl" - - # Choice of Python runtime version - - id: python-version - display_name: "Python" - values: - # Note: always display platform with python-version to avoid ambiguous display names. - # Linux - - id: "3.9" - display_name: "Python 3.9" - variables: - PYTHON_BINARY: "/opt/python/3.9/bin/python3" - - id: "3.10" - display_name: "Python 3.10" - variables: - PYTHON_BINARY: "/opt/python/3.10/bin/python3" - - id: "3.11" - display_name: "Python 3.11" - variables: - PYTHON_BINARY: "/opt/python/3.11/bin/python3" - - id: "3.12" - display_name: "Python 3.12" - variables: - PYTHON_BINARY: "/opt/python/3.12/bin/python3" - - id: "3.13" - display_name: "Python 3.13" - variables: - PYTHON_BINARY: "/opt/python/3.13/bin/python3" - - id: "pypy3.9" - display_name: "PyPy 3.9" - variables: - PYTHON_BINARY: "/opt/python/pypy3.9/bin/pypy3" - - id: "pypy3.10" - display_name: "PyPy 3.10" - variables: - PYTHON_BINARY: "/opt/python/pypy3.10/bin/pypy3" - - - id: python-version-windows - display_name: "Python" - values: - - id: "3.9" - display_name: "Python 3.9" - variables: - PYTHON_BINARY: "C:/python/Python39/python.exe" - - id: "3.10" - display_name: "Python 3.10" - variables: - PYTHON_BINARY: "C:/python/Python310/python.exe" - - id: "3.11" - display_name: "Python 3.11" - variables: - PYTHON_BINARY: "C:/python/Python311/python.exe" - - id: "3.12" - display_name: "Python 3.12" - variables: - PYTHON_BINARY: "C:/python/Python312/python.exe" - - id: "3.13" - display_name: "Python 3.13" - variables: - PYTHON_BINARY: "C:/python/Python313/python.exe" - buildvariants: -# Server Tests. -- name: test-rhel8-py3.9-auth-ssl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.9 Auth SSL cov - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.9-noauth-ssl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.9 NoAuth SSL cov - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.9-noauth-nossl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.9 NoAuth NoSSL cov - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: nossl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.13-auth-ssl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.13 Auth SSL cov - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.13-noauth-ssl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.13 NoAuth SSL cov +- name: "no-server" + display_name: "No server" run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.13-noauth-nossl-cov + - rhel84-small tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 py3.13 NoAuth NoSSL cov + - name: "no-server" + +- name: "Coverage Report" + display_name: "Coverage Report" run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: nossl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-pypy3.10-auth-ssl-cov + - rhel84-small tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 pypy3.10 Auth SSL cov + - name: "coverage-report" + +- name: testkms-variant + display_name: "KMS" run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-pypy3.10-noauth-ssl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 pypy3.10 NoAuth SSL cov - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: ssl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-pypy3.10-noauth-nossl-cov - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Test RHEL8 pypy3.10 NoAuth NoSSL cov - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: nossl - COVERAGE: coverage - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [coverage_tag] -- name: test-rhel8-py3.10-auth-ssl - tasks: - - name: .standalone - display_name: Test RHEL8 py3.10 Auth SSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: test-rhel8-py3.11-noauth-ssl - tasks: - - name: .replica_set - display_name: Test RHEL8 py3.11 NoAuth SSL - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: ssl - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: test-rhel8-py3.12-noauth-nossl - tasks: - - name: .sharded_cluster - display_name: Test RHEL8 py3.12 NoAuth NoSSL - run_on: - - rhel87-small - expansions: - AUTH: noauth - SSL: nossl - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: test-rhel8-pypy3.9-auth-ssl - tasks: - - name: .standalone - display_name: Test RHEL8 pypy3.9 Auth SSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: test-macos-py3.9-auth-ssl-sync - tasks: - - name: .standalone - display_name: Test macOS py3.9 Auth SSL Sync - run_on: - - macos-14 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.9-noauth-ssl-sync - tasks: - - name: .standalone - display_name: Test macOS py3.9 NoAuth SSL Sync - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.9-noauth-nossl-sync - tasks: - - name: .standalone - display_name: Test macOS py3.9 NoAuth NoSSL Sync - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.9-auth-ssl-async - tasks: - - name: .standalone - display_name: Test macOS py3.9 Auth SSL Async - run_on: - - macos-14 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.9-noauth-ssl-async - tasks: - - name: .standalone - display_name: Test macOS py3.9 NoAuth SSL Async - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.9-noauth-nossl-async - tasks: - - name: .standalone - display_name: Test macOS py3.9 NoAuth NoSSL Async - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-py3.13-auth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 Auth SSL Sync - run_on: - - macos-14 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-py3.13-noauth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 NoAuth SSL Sync - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-py3.13-noauth-nossl-sync - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 NoAuth NoSSL Sync - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-py3.13-auth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 Auth SSL Async - run_on: - - macos-14 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-py3.13-noauth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 NoAuth SSL Async - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-py3.13-noauth-nossl-async - tasks: - - name: .sharded_cluster - display_name: Test macOS py3.13 NoAuth NoSSL Async - run_on: - - macos-14 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.9-auth-ssl-sync - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 Auth SSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-noauth-ssl-sync - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 NoAuth SSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-noauth-nossl-sync - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-auth-ssl-async - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 Auth SSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-noauth-ssl-async - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 NoAuth SSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.9-noauth-nossl-async - tasks: - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Test macOS Arm64 py3.9 NoAuth NoSSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: test-macos-arm64-py3.13-auth-ssl-sync - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 Auth SSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-noauth-ssl-sync - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 NoAuth SSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-noauth-nossl-sync - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Sync - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-auth-ssl-async - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 Auth SSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-noauth-ssl-async - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 NoAuth SSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-macos-arm64-py3.13-noauth-nossl-async - tasks: - - name: .sharded_cluster .6.0 - - name: .sharded_cluster .7.0 - - name: .sharded_cluster .8.0 - - name: .sharded_cluster .rapid - - name: .sharded_cluster .latest - display_name: Test macOS Arm64 py3.13 NoAuth NoSSL Async - run_on: - - macos-14-arm64 - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 -- name: test-win64-py3.9-auth-ssl-sync - tasks: - - name: .standalone - display_name: Test Win64 py3.9 Auth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.9-noauth-ssl-sync - tasks: - - name: .standalone - display_name: Test Win64 py3.9 NoAuth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.9-noauth-nossl-sync - tasks: - - name: .standalone - display_name: Test Win64 py3.9 NoAuth NoSSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.9-auth-ssl-async - tasks: - - name: .standalone - display_name: Test Win64 py3.9 Auth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.9-noauth-ssl-async - tasks: - - name: .standalone - display_name: Test Win64 py3.9 NoAuth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.9-noauth-nossl-async - tasks: - - name: .standalone - display_name: Test Win64 py3.9 NoAuth NoSSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: test-win64-py3.13-auth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 Auth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win64-py3.13-noauth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 NoAuth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win64-py3.13-noauth-nossl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 NoAuth NoSSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win64-py3.13-auth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 Auth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win64-py3.13-noauth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 NoAuth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win64-py3.13-noauth-nossl-async - tasks: - - name: .sharded_cluster - display_name: Test Win64 py3.13 NoAuth NoSSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: test-win32-py3.9-auth-ssl-sync - tasks: - - name: .standalone - display_name: Test Win32 py3.9 Auth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.9-noauth-ssl-sync - tasks: - - name: .standalone - display_name: Test Win32 py3.9 NoAuth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.9-noauth-nossl-sync - tasks: - - name: .standalone - display_name: Test Win32 py3.9 NoAuth NoSSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.9-auth-ssl-async - tasks: - - name: .standalone - display_name: Test Win32 py3.9 Auth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.9-noauth-ssl-async - tasks: - - name: .standalone - display_name: Test Win32 py3.9 NoAuth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.9-noauth-nossl-async - tasks: - - name: .standalone - display_name: Test Win32 py3.9 NoAuth NoSSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python39/python.exe -- name: test-win32-py3.13-auth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 Auth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe -- name: test-win32-py3.13-noauth-ssl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 NoAuth SSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe -- name: test-win32-py3.13-noauth-nossl-sync - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 NoAuth NoSSL Sync - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe -- name: test-win32-py3.13-auth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 Auth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe -- name: test-win32-py3.13-noauth-ssl-async - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 NoAuth SSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: ssl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe -- name: test-win32-py3.13-noauth-nossl-async - tasks: - - name: .sharded_cluster - display_name: Test Win32 py3.13 NoAuth NoSSL Async - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: noauth - SSL: nossl - TEST_SUITES: default_async - SKIP_CSOT_TESTS: "true" - PYTHON_BINARY: C:/python/32/Python313/python.exe - -# Encryption tests. -- name: encryption-rhel8-py3.9-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption RHEL8 py3.9 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [encryption_tag] -- name: encryption-rhel8-py3.13-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption RHEL8 py3.13 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [encryption_tag] -- name: encryption-rhel8-pypy3.10-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption RHEL8 pypy3.10 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [encryption_tag] -- name: encryption-crypt_shared-rhel8-py3.9-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption crypt_shared RHEL8 py3.9 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [encryption_tag] -- name: encryption-crypt_shared-rhel8-py3.13-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption crypt_shared RHEL8 py3.13 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [encryption_tag] -- name: encryption-crypt_shared-rhel8-pypy3.10-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption crypt_shared RHEL8 pypy3.10 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [encryption_tag] -- name: encryption-pyopenssl-rhel8-py3.9-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption PyOpenSSL RHEL8 py3.9 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_encryption_pyopenssl: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [encryption_tag] -- name: encryption-pyopenssl-rhel8-py3.13-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption PyOpenSSL RHEL8 py3.13 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_encryption_pyopenssl: "true" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [encryption_tag] -- name: encryption-pyopenssl-rhel8-pypy3.10-auth-ssl - tasks: - - name: .standalone - - name: .replica_set - - name: .sharded_cluster - display_name: Encryption PyOpenSSL RHEL8 pypy3.10 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_encryption_pyopenssl: "true" - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - tags: [encryption_tag] -- name: encryption-rhel8-py3.10-auth-ssl - tasks: - - name: .replica_set - display_name: Encryption RHEL8 py3.10 Auth SSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: encryption-crypt_shared-rhel8-py3.11-auth-nossl - tasks: - - name: .replica_set - display_name: Encryption crypt_shared RHEL8 py3.11 Auth NoSSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: encryption-pyopenssl-rhel8-py3.12-auth-ssl - tasks: - - name: .replica_set - display_name: Encryption PyOpenSSL RHEL8 py3.12 Auth SSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - TEST_ENCRYPTION_PYOPENSSL: "true" - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: encryption-rhel8-pypy3.9-auth-nossl - tasks: - - name: .replica_set - display_name: Encryption RHEL8 pypy3.9 Auth NoSSL - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: encryption-macos-py3.9-auth-ssl - tasks: - - name: .latest .replica_set - display_name: Encryption macOS py3.9 Auth SSL - run_on: - - macos-14 - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 - tags: [encryption_tag] -- name: encryption-macos-py3.13-auth-nossl - tasks: - - name: .latest .replica_set - display_name: Encryption macOS py3.13 Auth NoSSL - run_on: - - macos-14 - batchtime: 10080 - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - tags: [encryption_tag] -- name: encryption-crypt_shared-macos-py3.9-auth-ssl - tasks: - - name: .latest .replica_set - display_name: Encryption crypt_shared macOS py3.9 Auth SSL - run_on: - - macos-14 - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 - tags: [encryption_tag] -- name: encryption-crypt_shared-macos-py3.13-auth-nossl - tasks: - - name: .latest .replica_set - display_name: Encryption crypt_shared macOS py3.13 Auth NoSSL - run_on: - - macos-14 - batchtime: 10080 - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - tags: [encryption_tag] -- name: encryption-win64-py3.9-auth-ssl - tasks: - - name: .latest .replica_set - display_name: Encryption Win64 py3.9 Auth SSL - run_on: - - windows-64-vsMulti-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - PYTHON_BINARY: C:/python/Python39/python.exe - tags: [encryption_tag] -- name: encryption-win64-py3.13-auth-nossl - tasks: - - name: .latest .replica_set - display_name: Encryption Win64 py3.13 Auth NoSSL - run_on: - - windows-64-vsMulti-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - PYTHON_BINARY: C:/python/Python313/python.exe - tags: [encryption_tag] -- name: encryption-crypt_shared-win64-py3.9-auth-ssl - tasks: - - name: .latest .replica_set - display_name: Encryption crypt_shared Win64 py3.9 Auth SSL - run_on: - - windows-64-vsMulti-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: ssl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: C:/python/Python39/python.exe - tags: [encryption_tag] -- name: encryption-crypt_shared-win64-py3.13-auth-nossl - tasks: - - name: .latest .replica_set - display_name: Encryption crypt_shared Win64 py3.13 Auth NoSSL - run_on: - - windows-64-vsMulti-small - batchtime: 10080 - expansions: - AUTH: auth - SSL: nossl - test_encryption: "true" - test_crypt_shared: "true" - PYTHON_BINARY: C:/python/Python313/python.exe - tags: [encryption_tag] - -# Compressor tests. -- name: snappy-compression-rhel8-py3.9-no-c - tasks: - - name: .standalone - display_name: snappy compression RHEL8 py3.9 No C - run_on: - - rhel87-small - expansions: - COMPRESSORS: snappy - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: snappy-compression-rhel8-py3.10 - tasks: - - name: .standalone - display_name: snappy compression RHEL8 py3.10 - run_on: - - rhel87-small - expansions: - COMPRESSORS: snappy - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: zlib-compression-rhel8-py3.11-no-c - tasks: - - name: .standalone - display_name: zlib compression RHEL8 py3.11 No C - run_on: - - rhel87-small - expansions: - COMPRESSORS: zlib - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: zlib-compression-rhel8-py3.12 - tasks: - - name: .standalone - display_name: zlib compression RHEL8 py3.12 - run_on: - - rhel87-small - expansions: - COMPRESSORS: zlib - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: zstd-compression-rhel8-py3.13-no-c - tasks: - - name: .standalone !.4.0 - display_name: zstd compression RHEL8 py3.13 No C - run_on: - - rhel87-small - expansions: - COMPRESSORS: zstd - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: zstd-compression-rhel8-py3.9 - tasks: - - name: .standalone !.4.0 - display_name: zstd compression RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - COMPRESSORS: zstd - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: snappy-compression-rhel8-pypy3.9 - tasks: - - name: .standalone - display_name: snappy compression RHEL8 pypy3.9 - run_on: - - rhel87-small - expansions: - COMPRESSORS: snappy - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: zlib-compression-rhel8-pypy3.10 - tasks: - - name: .standalone - display_name: zlib compression RHEL8 pypy3.10 - run_on: - - rhel87-small - expansions: - COMPRESSORS: zlib - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- name: zstd-compression-rhel8-pypy3.9 - tasks: - - name: .standalone !.4.0 - display_name: zstd compression RHEL8 pypy3.9 - run_on: - - rhel87-small - expansions: - COMPRESSORS: zstd - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 - -# Enterprise auth tests. -- name: enterprise-auth-macos-py3.9-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth macOS py3.9 Auth - run_on: - - macos-14 - expansions: - AUTH: auth - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: enterprise-auth-rhel8-py3.10-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.10 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: enterprise-auth-rhel8-py3.11-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.11 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: enterprise-auth-rhel8-py3.12-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 py3.12 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: enterprise-auth-win64-py3.13-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth Win64 py3.13 Auth - run_on: - - windows-64-vsMulti-small - expansions: - AUTH: auth - PYTHON_BINARY: C:/python/Python313/python.exe -- name: enterprise-auth-rhel8-pypy3.9-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 pypy3.9 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: enterprise-auth-rhel8-pypy3.10-auth - tasks: - - name: test-enterprise-auth - display_name: Enterprise Auth RHEL8 pypy3.10 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - -# PyOpenSSL tests. -- name: pyopenssl-macos-py3.9 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL macOS py3.9 - run_on: - - macos-14 - batchtime: 10080 - expansions: - AUTH: noauth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: pyopenssl-rhel8-py3.10 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.10 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: pyopenssl-rhel8-py3.11 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.11 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: pyopenssl-rhel8-py3.12 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 py3.12 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: pyopenssl-win64-py3.13 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL Win64 py3.13 - run_on: - - windows-64-vsMulti-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: C:/python/Python313/python.exe -- name: pyopenssl-rhel8-pypy3.9 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 pypy3.9 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: pyopenssl-rhel8-pypy3.10 - tasks: - - name: .replica_set - - name: .7.0 - display_name: PyOpenSSL RHEL8 pypy3.10 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - AUTH: auth - test_pyopenssl: "true" - SSL: ssl - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 - -# Storage Engine tests. -- name: storage-inmemory-rhel8-py3.9 - tasks: - - name: .standalone .4.0 - - name: .standalone .4.4 - - name: .standalone .5.0 - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Storage InMemory RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - STORAGE_ENGINE: inmemory - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: storage-mmapv1-rhel8-py3.9 - tasks: - - name: .standalone .4.0 - - name: .replica_set .4.0 - display_name: Storage MMAPv1 RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - STORAGE_ENGINE: mmapv1 - PYTHON_BINARY: /opt/python/3.9/bin/python3 - -# Versioned API tests. -- name: versioned-api-require-v1-rhel8-py3.9-auth - tasks: - - name: .standalone .5.0 - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Versioned API require v1 RHEL8 py3.9 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - REQUIRE_API_VERSION: "1" - MONGODB_API_VERSION: "1" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [versionedApi_tag] -- name: versioned-api-accept-v2-rhel8-py3.9-auth - tasks: - - name: .standalone .5.0 - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Versioned API accept v2 RHEL8 py3.9 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - ORCHESTRATION_FILE: versioned-api-testing.json - PYTHON_BINARY: /opt/python/3.9/bin/python3 - tags: [versionedApi_tag] -- name: versioned-api-require-v1-rhel8-py3.13-auth - tasks: - - name: .standalone .5.0 - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Versioned API require v1 RHEL8 py3.13 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - REQUIRE_API_VERSION: "1" - MONGODB_API_VERSION: "1" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [versionedApi_tag] -- name: versioned-api-accept-v2-rhel8-py3.13-auth - tasks: - - name: .standalone .5.0 - - name: .standalone .6.0 - - name: .standalone .7.0 - - name: .standalone .8.0 - - name: .standalone .rapid - - name: .standalone .latest - display_name: Versioned API accept v2 RHEL8 py3.13 Auth - run_on: - - rhel87-small - expansions: - AUTH: auth - ORCHESTRATION_FILE: versioned-api-testing.json - PYTHON_BINARY: /opt/python/3.13/bin/python3 - tags: [versionedApi_tag] - -# Green framework tests. -- name: eventlet-rhel8-py3.9 - tasks: - - name: .standalone - display_name: Eventlet RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - GREEN_FRAMEWORK: eventlet - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: gevent-rhel8-py3.9 - tasks: - - name: .standalone - display_name: Gevent RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - GREEN_FRAMEWORK: gevent - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: eventlet-rhel8-py3.12 - tasks: - - name: .standalone - display_name: Eventlet RHEL8 py3.12 - run_on: - - rhel87-small - expansions: - GREEN_FRAMEWORK: eventlet - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: gevent-rhel8-py3.12 - tasks: - - name: .standalone - display_name: Gevent RHEL8 py3.12 - run_on: - - rhel87-small - expansions: - GREEN_FRAMEWORK: gevent - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.12/bin/python3 - -# No C Ext tests. -- name: no-c-ext-rhel8-py3.9 - tasks: - - name: .standalone - display_name: No C Ext RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: no-c-ext-rhel8-py3.10 - tasks: - - name: .replica_set - display_name: No C Ext RHEL8 py3.10 - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: no-c-ext-rhel8-py3.11 - tasks: - - name: .sharded_cluster - display_name: No C Ext RHEL8 py3.11 - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: no-c-ext-rhel8-py3.12 - tasks: - - name: .standalone - display_name: No C Ext RHEL8 py3.12 - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: no-c-ext-rhel8-py3.13 - tasks: - - name: .replica_set - display_name: No C Ext RHEL8 py3.13 - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - -# Atlas Data Lake tests. -- name: atlas-data-lake-rhel8-py3.9-no-c - tasks: - - name: atlas-data-lake-tests - display_name: Atlas Data Lake RHEL8 py3.9 No C - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: atlas-data-lake-rhel8-py3.9 - tasks: - - name: atlas-data-lake-tests - display_name: Atlas Data Lake RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: atlas-data-lake-rhel8-py3.13-no-c - tasks: - - name: atlas-data-lake-tests - display_name: Atlas Data Lake RHEL8 py3.13 No C - run_on: - - rhel87-small - expansions: - NO_EXT: "1" - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: atlas-data-lake-rhel8-py3.13 - tasks: - - name: atlas-data-lake-tests - display_name: Atlas Data Lake RHEL8 py3.13 - run_on: - - rhel87-small - expansions: - PYTHON_BINARY: /opt/python/3.13/bin/python3 - -# Mod_wsgi tests. -- name: mod_wsgi-ubuntu-22-py3.9 - tasks: - - name: mod-wsgi-standalone - - name: mod-wsgi-replica-set - - name: mod-wsgi-embedded-mode-standalone - - name: mod-wsgi-embedded-mode-replica-set - display_name: mod_wsgi Ubuntu-22 py3.9 - run_on: - - ubuntu2204-small - expansions: - MOD_WSGI_VERSION: "4" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: mod_wsgi-ubuntu-22-py3.13 - tasks: - - name: mod-wsgi-standalone - - name: mod-wsgi-replica-set - - name: mod-wsgi-embedded-mode-standalone - - name: mod-wsgi-embedded-mode-replica-set - display_name: mod_wsgi Ubuntu-22 py3.13 - run_on: - - ubuntu2204-small - expansions: - MOD_WSGI_VERSION: "4" - PYTHON_BINARY: /opt/python/3.13/bin/python3 - -# Disable test commands variants. -- name: disable-test-commands-rhel8-py3.9 - tasks: - - name: .latest - display_name: Disable test commands RHEL8 py3.9 - run_on: - - rhel87-small - expansions: - AUTH: auth - SSL: ssl - DISABLE_TEST_COMMANDS: "1" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - -# Serverless variants. -- name: serverless-rhel8-py3.9 - tasks: - - name: serverless_task_group - display_name: Serverless RHEL8 py3.9 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - test_serverless: "true" - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: serverless-rhel8-py3.13 - tasks: - - name: serverless_task_group - display_name: Serverless RHEL8 py3.13 - run_on: - - rhel87-small - batchtime: 10080 - expansions: - test_serverless: "true" - AUTH: auth - SSL: ssl - PYTHON_BINARY: /opt/python/3.13/bin/python3 - -# AWS Auth tests. -- name: aws-auth-ubuntu-20-py3.9 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth Ubuntu-20 py3.9 - run_on: - - ubuntu2004-small - expansions: - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: aws-auth-ubuntu-20-py3.13 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth Ubuntu-20 py3.13 - run_on: - - ubuntu2004-small - expansions: - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: aws-auth-win64-py3.9 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth Win64 py3.9 - run_on: - - windows-64-vsMulti-small - expansions: - skip_ECS_auth_test: "true" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: aws-auth-win64-py3.13 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth Win64 py3.13 - run_on: - - windows-64-vsMulti-small - expansions: - skip_ECS_auth_test: "true" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: aws-auth-macos-py3.9 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth macOS py3.9 - run_on: - - macos-14 - expansions: - skip_ECS_auth_test: "true" - skip_EC2_auth_test: "true" - skip_web_identity_auth_test: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: aws-auth-macos-py3.13 - tasks: - - name: aws-auth-test-4.4 - - name: aws-auth-test-5.0 - - name: aws-auth-test-6.0 - - name: aws-auth-test-7.0 - - name: aws-auth-test-8.0 - - name: aws-auth-test-rapid - - name: aws-auth-test-latest - display_name: AWS Auth macOS py3.13 - run_on: - - macos-14 - expansions: - skip_ECS_auth_test: "true" - skip_EC2_auth_test: "true" - skip_web_identity_auth_test: "true" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - -- matrix_name: "tests-fips" - matrix_spec: - platform: - - rhel9-fips - auth: "auth" - ssl: "ssl" - display_name: "${platform} ${auth} ${ssl}" - tasks: - - "test-fips-standalone" - -# Test one server version with zSeries, POWER8, and ARM. -- matrix_name: "test-different-cpu-architectures" - matrix_spec: - platform: - - rhel8-zseries # Added in 5.0.8 (SERVER-44074) - - rhel8-power8 # Added in 4.2.7 (SERVER-44072) - - rhel8-arm64 # Added in 4.4.2 (SERVER-48282) - auth-ssl: "*" - display_name: "${platform} ${auth-ssl}" - tasks: - - ".6.0" - -- matrix_name: "tests-python-version-supports-openssl-102-test-ssl" - matrix_spec: - platform: rhel7 - # Python 3.10+ requires OpenSSL 1.1.1+ - python-version: ["3.9"] - auth-ssl: "*" - display_name: "OpenSSL 1.0.2 ${python-version} ${platform} ${auth-ssl}" - tasks: - - ".5.0" - -- matrix_name: "test-search-index-helpers" - matrix_spec: - platform: rhel8 - python-version: "3.9" - display_name: "Search Index Helpers ${platform}" - tasks: - - name: "test_atlas_task_group_search_indexes" - -- matrix_name: "mockupdb-tests" - matrix_spec: - platform: rhel8 - python-version: 3.9 - display_name: "MockupDB Tests" - tasks: - - name: "mockupdb" - -- matrix_name: "tests-doctests" - matrix_spec: - platform: rhel8 - python-version: ["3.9"] - display_name: "Doctests ${python-version} ${platform}" - tasks: - - name: "doctests" - -- name: "no-server" - display_name: "No server test" - run_on: - - rhel84-small - tasks: - - name: "no-server" - -- name: "Coverage Report" - display_name: "Coverage Report" - run_on: - - rhel84-small - tasks: - - name: "coverage-report" - -- matrix_name: "atlas-connect" - matrix_spec: - platform: rhel8 - python-version: "*" - display_name: "Atlas connect ${python-version} ${platform}" - tasks: - - name: "atlas-connect" - -# OCSP test matrix. -- name: ocsp-test-rhel8-v4.4-py3.9 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 v4.4 py3.9 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "4.4" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: ocsp-test-rhel8-v5.0-py3.10 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 v5.0 py3.10 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "5.0" - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: ocsp-test-rhel8-v6.0-py3.11 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 v6.0 py3.11 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "6.0" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: ocsp-test-rhel8-v7.0-py3.12 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 v7.0 py3.12 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "7.0" - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: ocsp-test-rhel8-v8.0-py3.13 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 v8.0 py3.13 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "8.0" - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: ocsp-test-rhel8-rapid-pypy3.9 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 rapid pypy3.9 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: rapid - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: ocsp-test-rhel8-latest-pypy3.10 - tasks: - - name: .ocsp - display_name: OCSP test RHEL8 latest pypy3.10 - run_on: - - rhel87-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: latest - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- name: ocsp-test-win64-v4.4-py3.9 - tasks: - - name: .ocsp-rsa !.ocsp-staple - display_name: OCSP test Win64 v4.4 py3.9 - run_on: - - windows-64-vsMulti-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "4.4" - PYTHON_BINARY: C:/python/Python39/python.exe -- name: ocsp-test-win64-v8.0-py3.13 - tasks: - - name: .ocsp-rsa !.ocsp-staple - display_name: OCSP test Win64 v8.0 py3.13 - run_on: - - windows-64-vsMulti-small - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "8.0" - PYTHON_BINARY: C:/python/Python313/python.exe -- name: ocsp-test-macos-v4.4-py3.9 - tasks: - - name: .ocsp-rsa !.ocsp-staple - display_name: OCSP test macOS v4.4 py3.9 - run_on: - - macos-14 - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "4.4" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3 -- name: ocsp-test-macos-v8.0-py3.13 - tasks: - - name: .ocsp-rsa !.ocsp-staple - display_name: OCSP test macOS v8.0 py3.13 - run_on: - - macos-14 - batchtime: 20160 - expansions: - AUTH: noauth - SSL: ssl - TOPOLOGY: server - VERSION: "8.0" - PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3 - -# Load balancer tests -- name: load-balancer-rhel8-v6.0-py3.9-auth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v6.0 py3.9 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "6.0" - AUTH: auth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: load-balancer-rhel8-v6.0-py3.10-noauth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v6.0 py3.10 NoAuth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "6.0" - AUTH: noauth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: load-balancer-rhel8-v6.0-py3.11-noauth-nossl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v6.0 py3.11 NoAuth NoSSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "6.0" - AUTH: noauth - SSL: nossl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: load-balancer-rhel8-v7.0-py3.12-auth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v7.0 py3.12 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "7.0" - AUTH: auth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: load-balancer-rhel8-v7.0-py3.13-noauth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v7.0 py3.13 NoAuth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "7.0" - AUTH: noauth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: load-balancer-rhel8-v7.0-pypy3.9-noauth-nossl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v7.0 pypy3.9 NoAuth NoSSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "7.0" - AUTH: noauth - SSL: nossl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: load-balancer-rhel8-v8.0-pypy3.10-auth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v8.0 pypy3.10 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "8.0" - AUTH: auth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- name: load-balancer-rhel8-v8.0-py3.9-noauth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v8.0 py3.9 NoAuth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "8.0" - AUTH: noauth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 -- name: load-balancer-rhel8-v8.0-py3.10-noauth-nossl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 v8.0 py3.10 NoAuth NoSSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: "8.0" - AUTH: noauth - SSL: nossl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.10/bin/python3 -- name: load-balancer-rhel8-latest-py3.11-auth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 latest py3.11 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: latest - AUTH: auth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.11/bin/python3 -- name: load-balancer-rhel8-latest-py3.12-noauth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 latest py3.12 NoAuth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: latest - AUTH: noauth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.12/bin/python3 -- name: load-balancer-rhel8-latest-py3.13-noauth-nossl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 latest py3.13 NoAuth NoSSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: latest - AUTH: noauth - SSL: nossl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.13/bin/python3 -- name: load-balancer-rhel8-rapid-pypy3.9-auth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 rapid pypy3.9 Auth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: rapid - AUTH: auth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/pypy3.9/bin/python3 -- name: load-balancer-rhel8-rapid-pypy3.10-noauth-ssl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 rapid pypy3.10 NoAuth SSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: rapid - AUTH: noauth - SSL: ssl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/pypy3.10/bin/python3 -- name: load-balancer-rhel8-rapid-py3.9-noauth-nossl - tasks: - - name: load-balancer-test - display_name: Load Balancer RHEL8 rapid py3.9 NoAuth NoSSL - run_on: - - rhel87-small - batchtime: 10080 - expansions: - VERSION: rapid - AUTH: noauth - SSL: nossl - test_loadbalancer: "true" - PYTHON_BINARY: /opt/python/3.9/bin/python3 - -- matrix_name: "oidc-auth-test" - matrix_spec: - platform: [ rhel8, macos, windows ] - display_name: "OIDC Auth ${platform}" - tasks: - - name: testoidc_task_group - batchtime: 20160 # 14 days - -- name: testazureoidc-variant - display_name: "OIDC Auth Azure" - run_on: ubuntu2204-small - tasks: - - name: testazureoidc_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - -- name: testgcpoidc-variant - display_name: "OIDC Auth GCP" - run_on: ubuntu2204-small - tasks: - - name: testgcpoidc_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - -- name: testgcpkms-variant - display_name: "GCP KMS" - run_on: - - debian11-small + - debian11-small tasks: - name: testgcpkms_task_group batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - testgcpkms-fail-task - -- name: testazurekms-variant - display_name: "Azure KMS" - run_on: debian11-small - tasks: - name: testazurekms_task_group batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - testazurekms-fail-task - name: rhel8-test-lambda - display_name: AWS Lambda handler tests + display_name: FaaS Lambda run_on: rhel87-small tasks: - name: test_aws_lambda_task_group -- name: rhel8-pr-assign-reviewer - display_name: Assign PR Reviewer - run_on: rhel87-small - tasks: - - name: "assign-pr-reviewer" - - name: rhel8-import-time - display_name: Import Time Check + display_name: Import Time run_on: rhel87-small tasks: - name: "check-import-time" @@ -4374,7 +1811,7 @@ buildvariants: - name: "backport-pr" - name: "perf-tests" - display_name: "Performance Benchmark Tests" + display_name: "Performance Benchmarks" batchtime: 10080 # 7 days run_on: rhel90-dbx-perf-large tasks: diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index 210a403d39..b7187b50db 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -9,13 +9,17 @@ # Note: Run this file with `hatch run`, `pipx run`, or `uv run`. from __future__ import annotations +import sys from dataclasses import dataclass +from inspect import getmembers, isfunction from itertools import cycle, product, zip_longest +from pathlib import Path from typing import Any from shrub.v3.evg_build_variant import BuildVariant +from shrub.v3.evg_command import FunctionCall from shrub.v3.evg_project import EvgProject -from shrub.v3.evg_task import EvgTaskRef +from shrub.v3.evg_task import EvgTask, EvgTaskRef from shrub.v3.shrub_service import ShrubService ############## @@ -23,7 +27,6 @@ ############## ALL_VERSIONS = ["4.0", "4.4", "5.0", "6.0", "7.0", "8.0", "rapid", "latest"] -VERSIONS_6_0_PLUS = ["6.0", "7.0", "8.0", "rapid", "latest"] CPYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] PYPYS = ["pypy3.9", "pypy3.10"] ALL_PYTHONS = CPYTHONS + PYPYS @@ -32,7 +35,13 @@ AUTH_SSLS = [("auth", "ssl"), ("noauth", "ssl"), ("noauth", "nossl")] TOPOLOGIES = ["standalone", "replica_set", "sharded_cluster"] C_EXTS = ["with_ext", "without_ext"] -SYNCS = ["sync", "async"] +# By default test each of the topologies with a subset of auth/ssl. +SUB_TASKS = [ + ".sharded_cluster .auth .ssl", + ".replica_set .noauth .ssl", + ".standalone .noauth .nossl", +] +SYNCS = ["sync", "async", "sync_async"] DISPLAY_LOOKUP = dict( ssl=dict(ssl="SSL", nossl="NoSSL"), auth=dict(auth="Auth", noauth="NoAuth"), @@ -172,6 +181,7 @@ def get_display_name(base: str, host: Host | None = None, **kwargs) -> str: if host is not None: display_name += f" {host.display_name}" version = kwargs.pop("VERSION", None) + version = version or kwargs.pop("version", None) if version: if version not in ["rapid", "latest"]: version = f"v{version}" @@ -222,7 +232,7 @@ def create_ocsp_variants() -> list[BuildVariant]: variants = [] batchtime = BATCHTIME_WEEK * 2 expansions = dict(AUTH="noauth", SSL="ssl", TOPOLOGY="server") - base_display = "OCSP test" + base_display = "OCSP" # OCSP tests on default host with all servers v4.4+ and all python versions. versions = [v for v in ALL_VERSIONS if v != "4.0"] @@ -230,7 +240,7 @@ def create_ocsp_variants() -> list[BuildVariant]: host = DEFAULT_HOST variant = create_variant( [".ocsp"], - get_display_name(base_display, host, version, python), + get_display_name(base_display, host, version=version, python=python), python=python, version=version, host=host, @@ -246,7 +256,7 @@ def create_ocsp_variants() -> list[BuildVariant]: python = CPYTHONS[0] if version == "4.4" else CPYTHONS[-1] variant = create_variant( [".ocsp-rsa !.ocsp-staple"], - get_display_name(base_display, host, version, python), + get_display_name(base_display, host, version=version, python=python), python=python, version=version, host=host, @@ -269,7 +279,7 @@ def create_server_variants() -> list[BuildVariant]: expansions = dict(COVERAGE="coverage") display_name = get_display_name(base_display_name, host, python=python, **expansions) variant = create_variant( - [f".{t}" for t in TOPOLOGIES], + [f".{t} .sync_async" for t in TOPOLOGIES], display_name, python=python, host=host, @@ -278,15 +288,12 @@ def create_server_variants() -> list[BuildVariant]: ) variants.append(variant) - # Test the rest of the pythons on linux. - for python, (auth, ssl), topology in zip_cycle( - CPYTHONS[1:-1] + PYPYS[:-1], AUTH_SSLS, TOPOLOGIES - ): + # Test the rest of the pythons. + for python in CPYTHONS[1:-1] + PYPYS[:-1]: display_name = f"Test {host}" - expansions = dict(AUTH=auth, SSL=ssl) - display_name = get_display_name("Test", host, python=python, **expansions) + display_name = get_display_name(base_display_name, host, python=python) variant = create_variant( - [f".{topology}"], + [f"{t} .sync_async" for t in SUB_TASKS], display_name, python=python, host=host, @@ -316,8 +323,8 @@ def create_encryption_variants() -> list[BuildVariant]: tags = ["encryption_tag"] batchtime = BATCHTIME_WEEK - def get_encryption_expansions(encryption, ssl="ssl"): - expansions = dict(AUTH="auth", SSL=ssl, test_encryption="true") + def get_encryption_expansions(encryption): + expansions = dict(test_encryption="true") if "crypt_shared" in encryption: expansions["test_crypt_shared"] = "true" if "PyOpenSSL" in encryption: @@ -326,13 +333,13 @@ def get_encryption_expansions(encryption, ssl="ssl"): host = DEFAULT_HOST - # Test against all server versions and topolgies for the three main python versions. + # Test against all server versions for the three main python versions. encryptions = ["Encryption", "Encryption crypt_shared", "Encryption PyOpenSSL"] for encryption, python in product(encryptions, [*MIN_MAX_PYTHON, PYPYS[-1]]): expansions = get_encryption_expansions(encryption) display_name = get_display_name(encryption, host, python=python, **expansions) variant = create_variant( - [f".{t}" for t in TOPOLOGIES], + [f"{t} .sync_async" for t in SUB_TASKS], display_name, python=python, host=host, @@ -343,13 +350,11 @@ def get_encryption_expansions(encryption, ssl="ssl"): variants.append(variant) # Test the rest of the pythons on linux for all server versions. - for encryption, python, ssl in zip_cycle( - encryptions, CPYTHONS[1:-1] + PYPYS[:-1], ["ssl", "nossl"] - ): - expansions = get_encryption_expansions(encryption, ssl) + for encryption, python, task in zip_cycle(encryptions, CPYTHONS[1:-1] + PYPYS[:-1], SUB_TASKS): + expansions = get_encryption_expansions(encryption) display_name = get_display_name(encryption, host, python=python, **expansions) variant = create_variant( - [".replica_set"], + [f"{task} .sync_async"], display_name, python=python, host=host, @@ -381,21 +386,17 @@ def create_load_balancer_variants(): # Load balancer tests - run all supported server versions using the lowest supported python. host = DEFAULT_HOST batchtime = BATCHTIME_WEEK - expansions_base = dict(test_loadbalancer="true") - versions = ["6.0", "7.0", "8.0", "latest", "rapid"] + versions = get_versions_from("6.0") variants = [] - pythons = CPYTHONS + PYPYS - for ind, (version, (auth, ssl)) in enumerate(product(versions, AUTH_SSLS)): - expansions = dict(VERSION=version, AUTH=auth, SSL=ssl) - expansions.update(expansions_base) - python = pythons[ind % len(pythons)] - display_name = get_display_name("Load Balancer", host, python=python, **expansions) + for version in versions: + python = CPYTHONS[0] + display_name = get_display_name("Load Balancer", host, python=python, version=version) variant = create_variant( - task_names, + [".load-balancer"], display_name, python=python, host=host, - expansions=expansions, + version=version, batchtime=batchtime, ) variants.append(variant) @@ -412,7 +413,7 @@ def create_compression_variants(): for ind, (compressor, c_ext) in enumerate(product(["snappy", "zlib", "zstd"], C_EXTS)): expansions = dict(COMPRESSORS=compressor) handle_c_ext(c_ext, expansions) - base_name = f"{compressor} compression" + base_name = f"Compression {compressor}" python = CPYTHONS[ind % len(CPYTHONS)] display_name = get_display_name(base_name, host, python=python, **expansions) variant = create_variant( @@ -428,7 +429,7 @@ def create_compression_variants(): for compressor, python in zip_cycle(["snappy", "zlib", "zstd"], other_pythons): expansions = dict(COMPRESSORS=compressor) handle_c_ext(c_ext, expansions) - base_name = f"{compressor} compression" + base_name = f"Compression {compressor}" display_name = get_display_name(base_name, host, python=python, **expansions) variant = create_variant( task_names[compressor], @@ -466,12 +467,13 @@ def create_enterprise_auth_variants(): def create_pyopenssl_variants(): base_name = "PyOpenSSL" batchtime = BATCHTIME_WEEK - base_expansions = dict(test_pyopenssl="true", SSL="ssl") + expansions = dict(test_pyopenssl="true") variants = [] for python in ALL_PYTHONS: # Only test "noauth" with min python. auth = "noauth" if python == CPYTHONS[0] else "auth" + ssl = "nossl" if auth == "noauth" else "ssl" if python == CPYTHONS[0]: host = HOSTS["macos"] elif python == CPYTHONS[-1]: @@ -481,7 +483,7 @@ def create_pyopenssl_variants(): display_name = get_display_name(base_name, host, python=python) variant = create_variant( - [".replica_set", ".7.0"], + [f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"], display_name, python=python, host=host, @@ -825,6 +827,66 @@ def create_load_balancer_tasks(): # Generate Config ################## -variants = create_pyopenssl_variants() -# print(len(variants)) -generate_yaml(variants=variants) + +def write_variants_to_file(): + mod = sys.modules[__name__] + here = Path(__file__).absolute().parent + target = here.parent / "generated_configs" / "variants.yml" + if target.exists(): + target.unlink() + with target.open("w") as fid: + fid.write("buildvariants:\n") + + for name, func in getmembers(mod, isfunction): + if not name.endswith("_variants"): + continue + if not name.startswith("create_"): + raise ValueError("Variant creators must start with create_") + title = name.replace("create_", "").replace("_variants", "").replace("_", " ").capitalize() + project = EvgProject(tasks=None, buildvariants=func()) + out = ShrubService.generate_yaml(project).splitlines() + with target.open("a") as fid: + fid.write(f" # {title} tests\n") + for line in out[1:]: + fid.write(f"{line}\n") + fid.write("\n") + + # Remove extra trailing newline: + data = target.read_text().splitlines() + with target.open("w") as fid: + for line in data[:-1]: + fid.write(f"{line}\n") + + +def write_tasks_to_file(): + mod = sys.modules[__name__] + here = Path(__file__).absolute().parent + target = here.parent / "generated_configs" / "tasks.yml" + if target.exists(): + target.unlink() + with target.open("w") as fid: + fid.write("tasks:\n") + + for name, func in getmembers(mod, isfunction): + if not name.endswith("_tasks"): + continue + if not name.startswith("create_"): + raise ValueError("Task creators must start with create_") + title = name.replace("create_", "").replace("_tasks", "").replace("_", " ").capitalize() + project = EvgProject(tasks=func(), buildvariants=None) + out = ShrubService.generate_yaml(project).splitlines() + with target.open("a") as fid: + fid.write(f" # {title} tests\n") + for line in out[1:]: + fid.write(f"{line}\n") + fid.write("\n") + + # Remove extra trailing newline: + data = target.read_text().splitlines() + with target.open("w") as fid: + for line in data[:-1]: + fid.write(f"{line}\n") + + +write_variants_to_file() +write_tasks_to_file() From 29c16dbac276181fdc16ad36162c6bf7c74c3f45 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Wed, 20 Nov 2024 16:14:18 -0500 Subject: [PATCH 12/14] PYTHON-4981 - Create workaround for asyncio.Task.cancelling support in older Python versions (#2009) --- pymongo/_asyncio_task.py | 49 +++++++++++++++++++++++++++++ pymongo/asynchronous/client_bulk.py | 1 - pymongo/asynchronous/encryption.py | 7 +++++ pymongo/asynchronous/monitor.py | 5 +++ pymongo/network_layer.py | 7 +++-- pymongo/periodic_executor.py | 12 ++++--- pymongo/synchronous/client_bulk.py | 1 - pymongo/synchronous/encryption.py | 7 +++++ pymongo/synchronous/monitor.py | 5 +++ test/__init__.py | 8 ++--- test/asynchronous/__init__.py | 8 ++--- test/asynchronous/test_client.py | 5 +++ test/test_client.py | 5 +++ 13 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 pymongo/_asyncio_task.py diff --git a/pymongo/_asyncio_task.py b/pymongo/_asyncio_task.py new file mode 100644 index 0000000000..8e457763d9 --- /dev/null +++ b/pymongo/_asyncio_task.py @@ -0,0 +1,49 @@ +# Copyright 2024-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. + +"""A custom asyncio.Task that allows checking if a task has been sent a cancellation request. +Can be removed once we drop Python 3.10 support in favor of asyncio.Task.cancelling.""" + + +from __future__ import annotations + +import asyncio +import sys +from typing import Any, Coroutine, Optional + + +# TODO (https://jira.mongodb.org/browse/PYTHON-4981): Revisit once the underlying cause of the swallowed cancellations is uncovered +class _Task(asyncio.Task): + def __init__(self, coro: Coroutine[Any, Any, Any], *, name: Optional[str] = None) -> None: + super().__init__(coro, name=name) + self._cancel_requests = 0 + asyncio._register_task(self) + + def cancel(self, msg: Optional[str] = None) -> bool: + self._cancel_requests += 1 + return super().cancel(msg=msg) + + def uncancel(self) -> int: + if self._cancel_requests > 0: + self._cancel_requests -= 1 + return self._cancel_requests + + def cancelling(self) -> int: + return self._cancel_requests + + +def create_task(coro: Coroutine[Any, Any, Any], *, name: Optional[str] = None) -> asyncio.Task: + if sys.version_info >= (3, 11): + return asyncio.create_task(coro, name=name) + return _Task(coro, name=name) diff --git a/pymongo/asynchronous/client_bulk.py b/pymongo/asynchronous/client_bulk.py index 0dcdaa6c07..45824256da 100644 --- a/pymongo/asynchronous/client_bulk.py +++ b/pymongo/asynchronous/client_bulk.py @@ -476,7 +476,6 @@ async def _process_results_cursor( if op_type == "delete": res = DeleteResult(doc, acknowledged=True) # type: ignore[assignment] full_result[f"{op_type}Results"][original_index] = res - except Exception as exc: # Attempt to close the cursor, then raise top-level error. if cmd_cursor.alive: diff --git a/pymongo/asynchronous/encryption.py b/pymongo/asynchronous/encryption.py index 735e543047..4802c3f54e 100644 --- a/pymongo/asynchronous/encryption.py +++ b/pymongo/asynchronous/encryption.py @@ -15,6 +15,7 @@ """Support for explicit client-side field level encryption.""" from __future__ import annotations +import asyncio import contextlib import enum import socket @@ -111,6 +112,8 @@ def _wrap_encryption_errors() -> Iterator[None]: # BSON encoding/decoding errors are unrelated to encryption so # we should propagate them unchanged. raise + except asyncio.CancelledError: + raise except Exception as exc: raise EncryptionError(exc) from exc @@ -200,6 +203,8 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None: conn.close() except (PyMongoError, MongoCryptError): raise # Propagate pymongo errors directly. + except asyncio.CancelledError: + raise except Exception as error: # Wrap I/O errors in PyMongo exceptions. _raise_connection_failure((host, port), error) @@ -722,6 +727,8 @@ async def create_encrypted_collection( await database.create_collection(name=name, **kwargs), encrypted_fields, ) + except asyncio.CancelledError: + raise except Exception as exc: raise EncryptedCollectionError(exc, encrypted_fields) from exc diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index 2ad57b03e7..ad1bc70aba 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -238,6 +238,9 @@ async def _run(self) -> None: except ReferenceError: # Topology was garbage-collected. await self.close() + finally: + if self._executor._stopped: + await self._rtt_monitor.close() async def _check_server(self) -> ServerDescription: """Call hello or read the next streaming response. @@ -254,6 +257,8 @@ async def _check_server(self) -> ServerDescription: details = cast(Mapping[str, Any], exc.details) await self._topology.receive_cluster_time(details.get("$clusterTime")) raise + except asyncio.CancelledError: + raise except ReferenceError: raise except Exception as error: diff --git a/pymongo/network_layer.py b/pymongo/network_layer.py index 377689047b..6ab6db2f7d 100644 --- a/pymongo/network_layer.py +++ b/pymongo/network_layer.py @@ -29,6 +29,7 @@ ) from pymongo import _csot, ssl_support +from pymongo._asyncio_task import create_task from pymongo.errors import _OperationCancelled from pymongo.socket_checker import _errno_from_exception @@ -259,12 +260,12 @@ async def async_receive_data( sock.settimeout(0.0) loop = asyncio.get_event_loop() - cancellation_task = asyncio.create_task(_poll_cancellation(conn)) + cancellation_task = create_task(_poll_cancellation(conn)) try: if _HAVE_SSL and isinstance(sock, (SSLSocket, _sslConn)): - read_task = asyncio.create_task(_async_receive_ssl(sock, length, loop)) # type: ignore[arg-type] + read_task = create_task(_async_receive_ssl(sock, length, loop)) # type: ignore[arg-type] else: - read_task = asyncio.create_task(_async_receive(sock, length, loop)) # type: ignore[arg-type] + read_task = create_task(_async_receive(sock, length, loop)) # type: ignore[arg-type] tasks = [read_task, cancellation_task] done, pending = await asyncio.wait( tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index 216a4457c7..2f89b91deb 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -23,6 +23,7 @@ import weakref from typing import Any, Optional +from pymongo._asyncio_task import create_task from pymongo.lock import _create_lock _IS_SYNC = False @@ -61,10 +62,11 @@ def __repr__(self) -> str: def open(self) -> None: """Start. Multiple calls have no effect.""" self._stopped = False - started = self._task and not self._task.done() - if not started: - self._task = asyncio.get_event_loop().create_task(self._run(), name=self._name) + if self._task is None or ( + self._task.done() and not self._task.cancelled() and not self._task.cancelling() # type: ignore[unused-ignore, attr-defined] + ): + self._task = create_task(self._run(), name=self._name) def close(self, dummy: Any = None) -> None: """Stop. To restart, call open(). @@ -83,7 +85,7 @@ async def join(self, timeout: Optional[int] = None) -> None: pass except asyncio.exceptions.CancelledError: # Task was already finished, or not yet started. - pass + raise def wake(self) -> None: """Execute the target function soon.""" @@ -97,6 +99,8 @@ def skip_sleep(self) -> None: async def _run(self) -> None: while not self._stopped: + if self._task and self._task.cancelling(): # type: ignore[unused-ignore, attr-defined] + raise asyncio.CancelledError try: if not await self._target(): self._stopped = True diff --git a/pymongo/synchronous/client_bulk.py b/pymongo/synchronous/client_bulk.py index 625e8429eb..9f6e3f7cf0 100644 --- a/pymongo/synchronous/client_bulk.py +++ b/pymongo/synchronous/client_bulk.py @@ -474,7 +474,6 @@ def _process_results_cursor( if op_type == "delete": res = DeleteResult(doc, acknowledged=True) # type: ignore[assignment] full_result[f"{op_type}Results"][original_index] = res - except Exception as exc: # Attempt to close the cursor, then raise top-level error. if cmd_cursor.alive: diff --git a/pymongo/synchronous/encryption.py b/pymongo/synchronous/encryption.py index 506ff8bcba..09d0c0f2fd 100644 --- a/pymongo/synchronous/encryption.py +++ b/pymongo/synchronous/encryption.py @@ -15,6 +15,7 @@ """Support for explicit client-side field level encryption.""" from __future__ import annotations +import asyncio import contextlib import enum import socket @@ -111,6 +112,8 @@ def _wrap_encryption_errors() -> Iterator[None]: # BSON encoding/decoding errors are unrelated to encryption so # we should propagate them unchanged. raise + except asyncio.CancelledError: + raise except Exception as exc: raise EncryptionError(exc) from exc @@ -200,6 +203,8 @@ def kms_request(self, kms_context: MongoCryptKmsContext) -> None: conn.close() except (PyMongoError, MongoCryptError): raise # Propagate pymongo errors directly. + except asyncio.CancelledError: + raise except Exception as error: # Wrap I/O errors in PyMongo exceptions. _raise_connection_failure((host, port), error) @@ -716,6 +721,8 @@ def create_encrypted_collection( database.create_collection(name=name, **kwargs), encrypted_fields, ) + except asyncio.CancelledError: + raise except Exception as exc: raise EncryptedCollectionError(exc, encrypted_fields) from exc diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index a0b7635ab1..df4130d4ab 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -238,6 +238,9 @@ def _run(self) -> None: except ReferenceError: # Topology was garbage-collected. self.close() + finally: + if self._executor._stopped: + self._rtt_monitor.close() def _check_server(self) -> ServerDescription: """Call hello or read the next streaming response. @@ -254,6 +257,8 @@ def _check_server(self) -> ServerDescription: details = cast(Mapping[str, Any], exc.details) self._topology.receive_cluster_time(details.get("$clusterTime")) raise + except asyncio.CancelledError: + raise except ReferenceError: raise except Exception as error: diff --git a/test/__init__.py b/test/__init__.py index dba3312424..d3a63db2d5 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -868,8 +868,9 @@ def reset_client_context(): if _IS_SYNC: # sync tests don't need to reset a client context return - client_context.client.close() - client_context.client = None + elif client_context.client is not None: + client_context.client.close() + client_context.client = None client_context._init_client() @@ -1135,7 +1136,7 @@ class IntegrationTest(PyMongoTestCase): @client_context.require_connection def setUp(self) -> None: - if not _IS_SYNC and client_context.client is not None: + if not _IS_SYNC: reset_client_context() if client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") @@ -1210,7 +1211,6 @@ def teardown(): c.drop_database("pymongo_test_mike") c.drop_database("pymongo_test_bernie") c.close() - print_running_clients() diff --git a/test/asynchronous/__init__.py b/test/asynchronous/__init__.py index bed49de161..73e2824742 100644 --- a/test/asynchronous/__init__.py +++ b/test/asynchronous/__init__.py @@ -870,8 +870,9 @@ async def reset_client_context(): if _IS_SYNC: # sync tests don't need to reset a client context return - await async_client_context.client.close() - async_client_context.client = None + elif async_client_context.client is not None: + await async_client_context.client.close() + async_client_context.client = None await async_client_context._init_client() @@ -1153,7 +1154,7 @@ class AsyncIntegrationTest(AsyncPyMongoTestCase): @async_client_context.require_connection async def asyncSetUp(self) -> None: - if not _IS_SYNC and async_client_context.client is not None: + if not _IS_SYNC: await reset_client_context() if async_client_context.load_balancer and not getattr(self, "RUN_ON_LOAD_BALANCER", False): raise SkipTest("this test does not support load balancers") @@ -1228,7 +1229,6 @@ async def async_teardown(): await c.drop_database("pymongo_test_mike") await c.drop_database("pymongo_test_bernie") await c.close() - print_running_clients() diff --git a/test/asynchronous/test_client.py b/test/asynchronous/test_client.py index 292a78d645..db232386ee 100644 --- a/test/asynchronous/test_client.py +++ b/test/asynchronous/test_client.py @@ -1280,6 +1280,7 @@ async def get_x(db): async def test_server_selection_timeout(self): client = AsyncMongoClient(serverSelectionTimeoutMS=100, connect=False) self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + await client.close() client = AsyncMongoClient(serverSelectionTimeoutMS=0, connect=False) @@ -1292,18 +1293,22 @@ async def test_server_selection_timeout(self): self.assertRaises( ConfigurationError, AsyncMongoClient, serverSelectionTimeoutMS=None, connect=False ) + await client.close() client = AsyncMongoClient( "mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False ) self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + await client.close() client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) self.assertAlmostEqual(0, client.options.server_selection_timeout) + await client.close() # Test invalid timeout in URI ignored and set to default. client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) self.assertAlmostEqual(30, client.options.server_selection_timeout) + await client.close() client = AsyncMongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) self.assertAlmostEqual(30, client.options.server_selection_timeout) diff --git a/test/test_client.py b/test/test_client.py index d41b0bbfda..5ec425f312 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -1243,6 +1243,7 @@ def get_x(db): def test_server_selection_timeout(self): client = MongoClient(serverSelectionTimeoutMS=100, connect=False) self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + client.close() client = MongoClient(serverSelectionTimeoutMS=0, connect=False) @@ -1253,16 +1254,20 @@ def test_server_selection_timeout(self): self.assertRaises( ConfigurationError, MongoClient, serverSelectionTimeoutMS=None, connect=False ) + client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=100", connect=False) self.assertAlmostEqual(0.1, client.options.server_selection_timeout) + client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=0", connect=False) self.assertAlmostEqual(0, client.options.server_selection_timeout) + client.close() # Test invalid timeout in URI ignored and set to default. client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=-1", connect=False) self.assertAlmostEqual(30, client.options.server_selection_timeout) + client.close() client = MongoClient("mongodb://localhost/?serverSelectionTimeoutMS=", connect=False) self.assertAlmostEqual(30, client.options.server_selection_timeout) From d8274b7bc758248b5921345690439e889fd122c3 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 26 Nov 2024 12:31:47 -0500 Subject: [PATCH 13/14] Fix encryption tests (#2018) --- .evergreen/config.yml | 2 +- test/asynchronous/test_encryption.py | 21 ++++++++++++--------- test/asynchronous/unified_format.py | 4 ---- test/asynchronous/utils_spec_runner.py | 2 -- test/test_encryption.py | 21 ++++++++++++--------- test/unified_format.py | 4 ---- test/utils_spec_runner.py | 2 -- 7 files changed, 25 insertions(+), 31 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 59b8a543fd..7ca3a72b1a 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -281,7 +281,7 @@ functions: "run tests": - command: subprocess.exec params: - include_expansions_in_env: ["TEST_DATA_LAKE", "AUTH", "SSL", "TEST_INDEX_MANAGEMENT", "CRYPT_SHARED_LIB_PATH", "test_encryption", "test_encryption_pyopenssl", "test_crypt_shared", "test_pyopenssl", "test_loadbalancer", "test_serverless", "ORCHESTRATION_FILE"] + include_expansions_in_env: ["TEST_DATA_LAKE", "PYTHON_BINARY", "AUTH", "SSL", "TEST_INDEX_MANAGEMENT", "CRYPT_SHARED_LIB_PATH", "test_encryption", "test_encryption_pyopenssl", "test_crypt_shared", "test_pyopenssl", "test_loadbalancer", "test_serverless", "ORCHESTRATION_FILE"] binary: bash working_dir: "src" args: diff --git a/test/asynchronous/test_encryption.py b/test/asynchronous/test_encryption.py index a34741c144..048db2d501 100644 --- a/test/asynchronous/test_encryption.py +++ b/test/asynchronous/test_encryption.py @@ -1234,7 +1234,9 @@ async def test_03_bulk_batch_split(self): doc2 = {"_id": "over_2mib_2", "unencrypted": "a" * _2_MiB} self.listener.reset() await self.coll_encrypted.bulk_write([InsertOne(doc1), InsertOne(doc2)]) - self.assertEqual(self.listener.started_command_names(), ["insert", "insert"]) + self.assertEqual( + len([c for c in self.listener.started_command_names() if c == "insert"]), 2 + ) async def test_04_bulk_batch_split(self): limits_doc = json_data("limits", "limits-doc.json") @@ -1244,7 +1246,9 @@ async def test_04_bulk_batch_split(self): doc2.update(limits_doc) self.listener.reset() await self.coll_encrypted.bulk_write([InsertOne(doc1), InsertOne(doc2)]) - self.assertEqual(self.listener.started_command_names(), ["insert", "insert"]) + self.assertEqual( + len([c for c in self.listener.started_command_names() if c == "insert"]), 2 + ) async def test_05_insert_succeeds_just_under_16MiB(self): doc = {"_id": "under_16mib", "unencrypted": "a" * (_16_MiB - 2000)} @@ -1482,19 +1486,18 @@ class AzureGCPEncryptionTestMixin(AsyncEncryptionIntegrationTest): KEYVAULT_COLL = "datakeys" client: AsyncMongoClient - async def asyncSetUp(self): - self.client = self.simple_client() + async def _setup(self): keyvault = self.client.get_database(self.KEYVAULT_DB).get_collection(self.KEYVAULT_COLL) await create_key_vault(keyvault, self.DEK) async def _test_explicit(self, expectation): + await self._setup() client_encryption = self.create_client_encryption( self.KMS_PROVIDER_MAP, # type: ignore[arg-type] ".".join([self.KEYVAULT_DB, self.KEYVAULT_COLL]), async_client_context.client, OPTS, ) - self.addAsyncCleanup(client_encryption.close) ciphertext = await client_encryption.encrypt( "string0", @@ -1506,6 +1509,7 @@ async def _test_explicit(self, expectation): self.assertEqual(await client_encryption.decrypt(ciphertext), "string0") async def _test_automatic(self, expectation_extjson, payload): + await self._setup() encrypted_db = "db" encrypted_coll = "coll" keyvault_namespace = ".".join([self.KEYVAULT_DB, self.KEYVAULT_COLL]) @@ -1520,7 +1524,6 @@ async def _test_automatic(self, expectation_extjson, payload): client = await self.async_rs_or_single_client( auto_encryption_opts=encryption_opts, event_listeners=[insert_listener] ) - self.addAsyncCleanup(client.aclose) coll = client.get_database(encrypted_db).get_collection( encrypted_coll, codec_options=OPTS, write_concern=WriteConcern("majority") @@ -1594,6 +1597,7 @@ async def test_automatic(self): # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#deadlock-tests class TestDeadlockProse(AsyncEncryptionIntegrationTest): async def asyncSetUp(self): + await super().asyncSetUp() self.client_test = await self.async_rs_or_single_client( maxPoolSize=1, readConcernLevel="majority", w="majority", uuidRepresentation="standard" ) @@ -1626,7 +1630,6 @@ async def asyncSetUp(self): self.ciphertext = await client_encryption.encrypt( "string0", Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_alt_name="local" ) - await client_encryption.close() self.client_listener = OvertCommandListener() self.topology_listener = TopologyEventListener() @@ -1821,6 +1824,7 @@ async def test_case_8(self): # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#14-decryption-events class TestDecryptProse(AsyncEncryptionIntegrationTest): async def asyncSetUp(self): + await super().asyncSetUp() self.client = async_client_context.client await self.client.db.drop_collection("decryption_events") await create_key_vault(self.client.keyvault.datakeys) @@ -2256,6 +2260,7 @@ async def test_06_named_kms_providers_apply_tls_options_kmip(self): # https://github.com/mongodb/specifications/blob/50e26fe/source/client-side-encryption/tests/README.md#unique-index-on-keyaltnames class TestUniqueIndexOnKeyAltNamesProse(AsyncEncryptionIntegrationTest): async def asyncSetUp(self): + await super().asyncSetUp() self.client = async_client_context.client await create_key_vault(self.client.keyvault.datakeys) kms_providers_map = {"local": {"key": LOCAL_MASTER_KEY}} @@ -2605,8 +2610,6 @@ async def AsyncMongoClient(**kwargs): assert isinstance(res["encrypted_indexed"], Binary) assert isinstance(res["encrypted_unindexed"], Binary) - await client_encryption.close() - # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#22-range-explicit-encryption class TestRangeQueryProse(AsyncEncryptionIntegrationTest): diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index ad02778315..b18b09383e 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -499,10 +499,6 @@ async def asyncSetUp(self): # process file-level runOnRequirements run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) if not await self.should_run_on(run_on_spec): - # Explicitly close async clients here - # to prevent leaky monitor tasks - if not _IS_SYNC: - await async_client_context.client.close() raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here diff --git a/test/asynchronous/utils_spec_runner.py b/test/asynchronous/utils_spec_runner.py index 9ce9ed6822..b79e5258b5 100644 --- a/test/asynchronous/utils_spec_runner.py +++ b/test/asynchronous/utils_spec_runner.py @@ -692,8 +692,6 @@ async def run_scenario(self, scenario_def, test): self.listener = listener self.pool_listener = pool_listener self.server_listener = server_listener - # Close the client explicitly to avoid having too many threads open. - self.addAsyncCleanup(client.close) # Create session0 and session1. sessions = {} diff --git a/test/test_encryption.py b/test/test_encryption.py index 6e6d8ec4d5..cb8bcb74d6 100644 --- a/test/test_encryption.py +++ b/test/test_encryption.py @@ -1230,7 +1230,9 @@ def test_03_bulk_batch_split(self): doc2 = {"_id": "over_2mib_2", "unencrypted": "a" * _2_MiB} self.listener.reset() self.coll_encrypted.bulk_write([InsertOne(doc1), InsertOne(doc2)]) - self.assertEqual(self.listener.started_command_names(), ["insert", "insert"]) + self.assertEqual( + len([c for c in self.listener.started_command_names() if c == "insert"]), 2 + ) def test_04_bulk_batch_split(self): limits_doc = json_data("limits", "limits-doc.json") @@ -1240,7 +1242,9 @@ def test_04_bulk_batch_split(self): doc2.update(limits_doc) self.listener.reset() self.coll_encrypted.bulk_write([InsertOne(doc1), InsertOne(doc2)]) - self.assertEqual(self.listener.started_command_names(), ["insert", "insert"]) + self.assertEqual( + len([c for c in self.listener.started_command_names() if c == "insert"]), 2 + ) def test_05_insert_succeeds_just_under_16MiB(self): doc = {"_id": "under_16mib", "unencrypted": "a" * (_16_MiB - 2000)} @@ -1476,19 +1480,18 @@ class AzureGCPEncryptionTestMixin(EncryptionIntegrationTest): KEYVAULT_COLL = "datakeys" client: MongoClient - def setUp(self): - self.client = self.simple_client() + def _setup(self): keyvault = self.client.get_database(self.KEYVAULT_DB).get_collection(self.KEYVAULT_COLL) create_key_vault(keyvault, self.DEK) def _test_explicit(self, expectation): + self._setup() client_encryption = self.create_client_encryption( self.KMS_PROVIDER_MAP, # type: ignore[arg-type] ".".join([self.KEYVAULT_DB, self.KEYVAULT_COLL]), client_context.client, OPTS, ) - self.addCleanup(client_encryption.close) ciphertext = client_encryption.encrypt( "string0", @@ -1500,6 +1503,7 @@ def _test_explicit(self, expectation): self.assertEqual(client_encryption.decrypt(ciphertext), "string0") def _test_automatic(self, expectation_extjson, payload): + self._setup() encrypted_db = "db" encrypted_coll = "coll" keyvault_namespace = ".".join([self.KEYVAULT_DB, self.KEYVAULT_COLL]) @@ -1514,7 +1518,6 @@ def _test_automatic(self, expectation_extjson, payload): client = self.rs_or_single_client( auto_encryption_opts=encryption_opts, event_listeners=[insert_listener] ) - self.addCleanup(client.close) coll = client.get_database(encrypted_db).get_collection( encrypted_coll, codec_options=OPTS, write_concern=WriteConcern("majority") @@ -1588,6 +1591,7 @@ def test_automatic(self): # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#deadlock-tests class TestDeadlockProse(EncryptionIntegrationTest): def setUp(self): + super().setUp() self.client_test = self.rs_or_single_client( maxPoolSize=1, readConcernLevel="majority", w="majority", uuidRepresentation="standard" ) @@ -1618,7 +1622,6 @@ def setUp(self): self.ciphertext = client_encryption.encrypt( "string0", Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, key_alt_name="local" ) - client_encryption.close() self.client_listener = OvertCommandListener() self.topology_listener = TopologyEventListener() @@ -1813,6 +1816,7 @@ def test_case_8(self): # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#14-decryption-events class TestDecryptProse(EncryptionIntegrationTest): def setUp(self): + super().setUp() self.client = client_context.client self.client.db.drop_collection("decryption_events") create_key_vault(self.client.keyvault.datakeys) @@ -2248,6 +2252,7 @@ def test_06_named_kms_providers_apply_tls_options_kmip(self): # https://github.com/mongodb/specifications/blob/50e26fe/source/client-side-encryption/tests/README.md#unique-index-on-keyaltnames class TestUniqueIndexOnKeyAltNamesProse(EncryptionIntegrationTest): def setUp(self): + super().setUp() self.client = client_context.client create_key_vault(self.client.keyvault.datakeys) kms_providers_map = {"local": {"key": LOCAL_MASTER_KEY}} @@ -2589,8 +2594,6 @@ def MongoClient(**kwargs): assert isinstance(res["encrypted_indexed"], Binary) assert isinstance(res["encrypted_unindexed"], Binary) - client_encryption.close() - # https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#22-range-explicit-encryption class TestRangeQueryProse(EncryptionIntegrationTest): diff --git a/test/unified_format.py b/test/unified_format.py index a12ef805f2..5cb268a29d 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -498,10 +498,6 @@ def setUp(self): # process file-level runOnRequirements run_on_spec = self.TEST_SPEC.get("runOnRequirements", []) if not self.should_run_on(run_on_spec): - # Explicitly close async clients here - # to prevent leaky monitor tasks - if not _IS_SYNC: - client_context.client.close() raise unittest.SkipTest(f"{self.__class__.__name__} runOnRequirements not satisfied") # add any special-casing for skipping tests here diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index c50d23175b..4508502cd0 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -689,8 +689,6 @@ def run_scenario(self, scenario_def, test): self.listener = listener self.pool_listener = pool_listener self.server_listener = server_listener - # Close the client explicitly to avoid having too many threads open. - self.addCleanup(client.close) # Create session0 and session1. sessions = {} From 29e924859d4262f2c68f905f881d24c6f147a2e1 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Tue, 26 Nov 2024 16:43:38 -0500 Subject: [PATCH 14/14] Fix THIRD-PARTY-NOTICES (#2019) --- THIRD-PARTY-NOTICES | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 3c52eac15b..ad00831a2a 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -39,41 +39,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -2) License Notice for bson-stdint-win32.h ------------------------------------------ - -ISO C9x compliant stdint.h for Microsoft Visual Studio -Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 - - Copyright (c) 2006-2013 Alexander Chemeris - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of the product nor the names of its contributors may - be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -3) License Notice for _asyncio_lock.py +2) License Notice for _asyncio_lock.py ----------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation