From 0423d60643e6a035aba0c6cc584da2163e33ae75 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 28 Jan 2025 15:53:05 -0800 Subject: [PATCH 1/9] WIP --- test/asynchronous/test_csot.py | 114 +++++++++++++++++++++++++++++++++ test/test_csot.py | 2 + tools/synchro.py | 1 + 3 files changed, 117 insertions(+) create mode 100644 test/asynchronous/test_csot.py diff --git a/test/asynchronous/test_csot.py b/test/asynchronous/test_csot.py new file mode 100644 index 0000000000..42f184c3a7 --- /dev/null +++ b/test/asynchronous/test_csot.py @@ -0,0 +1,114 @@ +# 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 CSOT unified spec tests.""" +from __future__ import annotations + +import os +import sys + +sys.path[0:0] = [""] + +from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest +from test.unified_format import generate_test_classes + +import pymongo +from pymongo import _csot +from pymongo.errors import PyMongoError + +_IS_SYNC = False + +# Location of JSON test specifications. +TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "csot") + +# Generate unified tests. +globals().update(generate_test_classes(TEST_PATH, module=__name__)) + + +class TestCSOT(AsyncIntegrationTest): + RUN_ON_SERVERLESS = True + RUN_ON_LOAD_BALANCER = True + + async def test_timeout_nested(self): + if os.environ.get("SKIP_CSOT_TESTS", ""): + raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...") + coll = self.db.coll + self.assertEqual(_csot.get_timeout(), None) + self.assertEqual(_csot.get_deadline(), float("inf")) + self.assertEqual(_csot.get_rtt(), 0.0) + with pymongo.timeout(10): + await coll.find_one() + self.assertEqual(_csot.get_timeout(), 10) + deadline_10 = _csot.get_deadline() + + # Capped at the original 10 deadline. + with pymongo.timeout(15): + await coll.find_one() + self.assertEqual(_csot.get_timeout(), 15) + self.assertEqual(_csot.get_deadline(), deadline_10) + + # Should be reset to previous values + self.assertEqual(_csot.get_timeout(), 10) + self.assertEqual(_csot.get_deadline(), deadline_10) + await coll.find_one() + + with pymongo.timeout(5): + await coll.find_one() + self.assertEqual(_csot.get_timeout(), 5) + self.assertLess(_csot.get_deadline(), deadline_10) + + # Should be reset to previous values + self.assertEqual(_csot.get_timeout(), 10) + self.assertEqual(_csot.get_deadline(), deadline_10) + await coll.find_one() + + # Should be reset to previous values + self.assertEqual(_csot.get_timeout(), None) + self.assertEqual(_csot.get_deadline(), float("inf")) + self.assertEqual(_csot.get_rtt(), 0.0) + + @async_client_context.require_change_streams + async def test_change_stream_can_resume_after_timeouts(self): + if os.environ.get("SKIP_CSOT_TESTS", ""): + raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...") + coll = self.db.test + await coll.insert_one({}) + async with await coll.watch() as stream: + with pymongo.timeout(0.1): + with self.assertRaises(PyMongoError) as ctx: + await stream.next() + self.assertTrue(ctx.exception.timeout) + self.assertTrue(stream.alive) + with self.assertRaises(PyMongoError) as ctx: + await stream.try_next() + self.assertTrue(ctx.exception.timeout) + self.assertTrue(stream.alive) + # Resume before the insert on 3.6 because 4.0 is required to avoid skipping documents + if async_client_context.version < (4, 0): + await stream.try_next() + await coll.insert_one({}) + with pymongo.timeout(10): + self.assertTrue(stream.next()) + self.assertTrue(stream.alive) + # Timeout applies to entire next() call, not only individual commands. + with pymongo.timeout(0.5): + with self.assertRaises(PyMongoError) as ctx: + await stream.next() + self.assertTrue(ctx.exception.timeout) + self.assertTrue(stream.alive) + self.assertFalse(stream.alive) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_csot.py b/test/test_csot.py index c075a07d5a..07b24e52e2 100644 --- a/test/test_csot.py +++ b/test/test_csot.py @@ -27,6 +27,8 @@ from pymongo import _csot from pymongo.errors import PyMongoError +_IS_SYNC = True + # Location of JSON test specifications. TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "csot") diff --git a/tools/synchro.py b/tools/synchro.py index dbcbbd1351..56dfc53923 100644 --- a/tools/synchro.py +++ b/tools/synchro.py @@ -201,6 +201,7 @@ def async_only_test(f: str) -> bool: "test_connections_survive_primary_stepdown_spec.py", "test_create_entities.py", "test_crud_unified.py", + "test_csot.py", "test_cursor.py", "test_database.py", "test_encryption.py", From b021ede6ed6e69c8e18753b04668a8d9280579a7 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Wed, 29 Jan 2025 09:58:56 -0800 Subject: [PATCH 2/9] fix test_path --- test/asynchronous/test_csot.py | 6 +++++- test/test_csot.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/test_csot.py b/test/asynchronous/test_csot.py index 42f184c3a7..ee8742b408 100644 --- a/test/asynchronous/test_csot.py +++ b/test/asynchronous/test_csot.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -30,7 +31,10 @@ _IS_SYNC = False # Location of JSON test specifications. -TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "csot") +if _IS_SYNC: + TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "csot") +else: + TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "csot") # Generate unified tests. globals().update(generate_test_classes(TEST_PATH, module=__name__)) diff --git a/test/test_csot.py b/test/test_csot.py index 07b24e52e2..32ea660463 100644 --- a/test/test_csot.py +++ b/test/test_csot.py @@ -16,6 +16,7 @@ from __future__ import annotations import os +import pathlib import sys sys.path[0:0] = [""] @@ -30,7 +31,10 @@ _IS_SYNC = True # Location of JSON test specifications. -TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "csot") +if _IS_SYNC: + TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "csot") +else: + TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "csot") # Generate unified tests. globals().update(generate_test_classes(TEST_PATH, module=__name__)) From 469013b2a4259b37a3af8d93022d4561089eefc1 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Wed, 29 Jan 2025 11:09:29 -0800 Subject: [PATCH 3/9] add missing await --- test/asynchronous/test_csot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/asynchronous/test_csot.py b/test/asynchronous/test_csot.py index ee8742b408..d0fe9c937d 100644 --- a/test/asynchronous/test_csot.py +++ b/test/asynchronous/test_csot.py @@ -103,7 +103,7 @@ async def test_change_stream_can_resume_after_timeouts(self): await stream.try_next() await coll.insert_one({}) with pymongo.timeout(10): - self.assertTrue(stream.next()) + self.assertTrue(await stream.next()) self.assertTrue(stream.alive) # Timeout applies to entire next() call, not only individual commands. with pymongo.timeout(0.5): From c36586b99214e99264487ad65fa2d7bb5fa102e1 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Wed, 29 Jan 2025 11:31:02 -0800 Subject: [PATCH 4/9] modify imports --- test/asynchronous/test_csot.py | 6 +++--- test/test_csot.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/asynchronous/test_csot.py b/test/asynchronous/test_csot.py index d0fe9c937d..adfb30bb5c 100644 --- a/test/asynchronous/test_csot.py +++ b/test/asynchronous/test_csot.py @@ -16,8 +16,8 @@ from __future__ import annotations import os -import pathlib import sys +from pathlib import Path sys.path[0:0] = [""] @@ -32,9 +32,9 @@ # Location of JSON test specifications. if _IS_SYNC: - TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "csot") + TEST_PATH = os.path.join(Path(__file__).resolve().parent, "csot") else: - TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "csot") + TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "csot") # Generate unified tests. globals().update(generate_test_classes(TEST_PATH, module=__name__)) diff --git a/test/test_csot.py b/test/test_csot.py index 32ea660463..5201156a1d 100644 --- a/test/test_csot.py +++ b/test/test_csot.py @@ -16,8 +16,8 @@ from __future__ import annotations import os -import pathlib import sys +from pathlib import Path sys.path[0:0] = [""] @@ -32,9 +32,9 @@ # Location of JSON test specifications. if _IS_SYNC: - TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "csot") + TEST_PATH = os.path.join(Path(__file__).resolve().parent, "csot") else: - TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "csot") + TEST_PATH = os.path.join(Path(__file__).resolve().parent.parent, "csot") # Generate unified tests. globals().update(generate_test_classes(TEST_PATH, module=__name__)) From 560b644c64173bb7b73d2695036fd811e0fdace9 Mon Sep 17 00:00:00 2001 From: Iris <58442094+sleepyStick@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:10:33 -0800 Subject: [PATCH 5/9] Update test/asynchronous/test_csot.py Co-authored-by: Noah Stapp --- test/asynchronous/test_csot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/asynchronous/test_csot.py b/test/asynchronous/test_csot.py index adfb30bb5c..9e928c2251 100644 --- a/test/asynchronous/test_csot.py +++ b/test/asynchronous/test_csot.py @@ -22,7 +22,7 @@ sys.path[0:0] = [""] from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest -from test.unified_format import generate_test_classes +from test.asynchronous.unified_format import generate_test_classes import pymongo from pymongo import _csot From 86dace26b3caafa82c82cbec65abbae62e44b4de Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Thu, 30 Jan 2025 11:45:36 -0800 Subject: [PATCH 6/9] remove async cleanup of kill all sessions --- test/asynchronous/unified_format.py | 1 - test/unified_format.py | 1 - 2 files changed, 2 deletions(-) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 52d964eb3e..3805cd2236 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -1387,7 +1387,6 @@ async def run_scenario(self, spec, uri=None): # 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. diff --git a/test/unified_format.py b/test/unified_format.py index 372eb8abba..d7dd18ab38 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -1374,7 +1374,6 @@ def run_scenario(self, spec, uri=None): # transaction (from a test failure) from blocking collection/database # operations during test set up and tear down. self.kill_all_sessions() - self.addCleanup(self.kill_all_sessions) if "csot" in self.id().lower(): # Retry CSOT tests up to 2 times to deal with flakey tests. From 7b2a920efd468d7e8fde0fc03ad2b22caea2c1d4 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Fri, 14 Feb 2025 09:37:10 -0800 Subject: [PATCH 7/9] retry flakey tests --- test/asynchronous/unified_format.py | 4 +++- test/unified_format.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 96a88e33c9..17fa9b3209 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -1394,7 +1394,9 @@ async def run_scenario(self, spec, uri=None): for i in range(attempts): try: return await self._run_scenario(spec, uri) - except AssertionError: + except (AssertionError, OperationFailure) as exc: + if isinstance(exc, OperationFailure) and "failpoint" not in exc._message: + raise if i < attempts - 1: print( f"Retrying after attempt {i+1} of {self.id()} failed with:\n" diff --git a/test/unified_format.py b/test/unified_format.py index 23495256e2..7031aee27a 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -1381,7 +1381,9 @@ def run_scenario(self, spec, uri=None): for i in range(attempts): try: return self._run_scenario(spec, uri) - except AssertionError: + except (AssertionError, OperationFailure) as exc: + if isinstance(exc, OperationFailure) and "failpoint" not in exc._message: + raise if i < attempts - 1: print( f"Retrying after attempt {i+1} of {self.id()} failed with:\n" From efad63b3a1c918d42db080766b8186c501285b29 Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 18 Feb 2025 10:50:17 -0800 Subject: [PATCH 8/9] only retry for async --- test/asynchronous/unified_format.py | 6 +++++- test/unified_format.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 17fa9b3209..551cb6a643 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -1395,7 +1395,11 @@ async def run_scenario(self, spec, uri=None): try: return await self._run_scenario(spec, uri) except (AssertionError, OperationFailure) as exc: - if isinstance(exc, OperationFailure) and "failpoint" not in exc._message: + if not ( + isinstance(exc, OperationFailure) + and not _IS_SYNC + and "failpoint" in exc._message + ): raise if i < attempts - 1: print( diff --git a/test/unified_format.py b/test/unified_format.py index 7031aee27a..7c57404062 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -1382,7 +1382,11 @@ def run_scenario(self, spec, uri=None): try: return self._run_scenario(spec, uri) except (AssertionError, OperationFailure) as exc: - if isinstance(exc, OperationFailure) and "failpoint" not in exc._message: + if not ( + isinstance(exc, OperationFailure) + and not _IS_SYNC + and "failpoint" in exc._message + ): raise if i < attempts - 1: print( From ab5361fb88902bf8ac839507d2bf50a89125e43f Mon Sep 17 00:00:00 2001 From: Iris Ho Date: Tue, 18 Feb 2025 11:42:48 -0800 Subject: [PATCH 9/9] change if stmt logic to be easier to read --- test/asynchronous/unified_format.py | 6 ++---- test/unified_format.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/asynchronous/unified_format.py b/test/asynchronous/unified_format.py index 551cb6a643..695f58ee27 100644 --- a/test/asynchronous/unified_format.py +++ b/test/asynchronous/unified_format.py @@ -1395,10 +1395,8 @@ async def run_scenario(self, spec, uri=None): try: return await self._run_scenario(spec, uri) except (AssertionError, OperationFailure) as exc: - if not ( - isinstance(exc, OperationFailure) - and not _IS_SYNC - and "failpoint" in exc._message + if isinstance(exc, OperationFailure) and ( + _IS_SYNC or "failpoint" not in exc._message ): raise if i < attempts - 1: diff --git a/test/unified_format.py b/test/unified_format.py index 7c57404062..73dee10ddf 100644 --- a/test/unified_format.py +++ b/test/unified_format.py @@ -1382,10 +1382,8 @@ def run_scenario(self, spec, uri=None): try: return self._run_scenario(spec, uri) except (AssertionError, OperationFailure) as exc: - if not ( - isinstance(exc, OperationFailure) - and not _IS_SYNC - and "failpoint" in exc._message + if isinstance(exc, OperationFailure) and ( + _IS_SYNC or "failpoint" not in exc._message ): raise if i < attempts - 1: