Skip to content

Commit 82a6392

Browse files
committed
PYTHON-5413 Handle flaky tests
1 parent 8a94de1 commit 82a6392

12 files changed

+92
-34
lines changed

.evergreen/scripts/setup_tests.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,6 @@ def handle_test_env() -> None:
162162
write_env("PIP_PREFER_BINARY") # Prefer binary dists by default.
163163
write_env("UV_FROZEN") # Do not modify lock files.
164164

165-
# Skip CSOT tests on non-linux platforms.
166-
if PLATFORM != "linux":
167-
write_env("SKIP_CSOT_TESTS")
168-
169165
# Set an environment variable for the test name and sub test name.
170166
write_env(f"TEST_{test_name.upper()}")
171167
write_env("TEST_NAME", test_name)

test/asynchronous/test_client_bulk_write.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
async_client_context,
2626
unittest,
2727
)
28+
from test.asynchronous.utils import flaky
2829
from test.utils_shared import (
2930
OvertCommandListener,
3031
)
@@ -619,15 +620,14 @@ async def test_15_unacknowledged_write_across_batches(self):
619620
# https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#11-multi-batch-bulkwrites
620621
class TestClientBulkWriteCSOT(AsyncIntegrationTest):
621622
async def asyncSetUp(self):
622-
if os.environ.get("SKIP_CSOT_TESTS", ""):
623-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
624623
await super().asyncSetUp()
625624
self.max_write_batch_size = await async_client_context.max_write_batch_size
626625
self.max_bson_object_size = await async_client_context.max_bson_size
627626
self.max_message_size_bytes = await async_client_context.max_message_size_bytes
628627

629628
@async_client_context.require_version_min(8, 0, 0, -24)
630629
@async_client_context.require_failCommand_fail_point
630+
@flaky
631631
async def test_timeout_in_multi_batch_bulk_write(self):
632632
_OVERHEAD = 500
633633

test/asynchronous/test_csot.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
2525
from test.asynchronous.unified_format import generate_test_classes
26+
from test.asynchronous.utils import flaky
2627

2728
import pymongo
2829
from pymongo import _csot
@@ -43,9 +44,8 @@
4344
class TestCSOT(AsyncIntegrationTest):
4445
RUN_ON_LOAD_BALANCER = True
4546

47+
@flaky
4648
async def test_timeout_nested(self):
47-
if os.environ.get("SKIP_CSOT_TESTS", ""):
48-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
4949
coll = self.db.coll
5050
self.assertEqual(_csot.get_timeout(), None)
5151
self.assertEqual(_csot.get_deadline(), float("inf"))
@@ -82,9 +82,8 @@ async def test_timeout_nested(self):
8282
self.assertEqual(_csot.get_rtt(), 0.0)
8383

8484
@async_client_context.require_change_streams
85+
@flaky
8586
async def test_change_stream_can_resume_after_timeouts(self):
86-
if os.environ.get("SKIP_CSOT_TESTS", ""):
87-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
8887
coll = self.db.test
8988
await coll.insert_one({})
9089
async with await coll.watch() as stream:

test/asynchronous/test_cursor.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
sys.path[0:0] = [""]
3232

3333
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
34+
from test.asynchronous.utils import flaky
3435
from test.utils_shared import (
3536
AllowListEventListener,
3637
EventListener,
@@ -1415,9 +1416,8 @@ async def test_to_list_length(self):
14151416
docs = await c.to_list(3)
14161417
self.assertEqual(len(docs), 2)
14171418

1419+
@flaky
14181420
async def test_to_list_csot_applied(self):
1419-
if os.environ.get("SKIP_CSOT_TESTS", ""):
1420-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
14211421
client = await self.async_single_client(timeoutMS=500, w=1)
14221422
coll = client.pymongo.test
14231423
# Initialize the client with a larger timeout to help make test less flakey
@@ -1458,9 +1458,8 @@ async def test_command_cursor_to_list_length(self):
14581458
self.assertEqual(len(await result.to_list(1)), 1)
14591459

14601460
@async_client_context.require_failCommand_blockConnection
1461+
@flaky
14611462
async def test_command_cursor_to_list_csot_applied(self):
1462-
if os.environ.get("SKIP_CSOT_TESTS", ""):
1463-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
14641463
client = await self.async_single_client(timeoutMS=500, w=1)
14651464
coll = client.pymongo.test
14661465
# Initialize the client with a larger timeout to help make test less flakey

test/asynchronous/unified_format.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
from test.unified_format_shared import (
4141
KMS_TLS_OPTS,
4242
PLACEHOLDER_MAP,
43-
SKIP_CSOT_TESTS,
4443
EventListenerUtil,
4544
MatchEvaluatorUtil,
4645
coerce_result,
@@ -1374,9 +1373,6 @@ async def verify_outcome(self, spec):
13741373
self.assertListEqual(sorted_expected_documents, actual_documents)
13751374

13761375
async def run_scenario(self, spec, uri=None):
1377-
if "csot" in self.id().lower() and SKIP_CSOT_TESTS:
1378-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
1379-
13801376
# Kill all sessions before and after each test to prevent an open
13811377
# transaction (from a test failure) from blocking collection/database
13821378
# operations during test set up and tear down.

test/asynchronous/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
import asyncio
1919
import contextlib
2020
import random
21+
import sys
2122
import threading # Used in the synchronized version of this file
2223
import time
2324
from asyncio import iscoroutinefunction
25+
from functools import wraps
2426

2527
from bson.son import SON
2628
from pymongo import AsyncMongoClient
@@ -154,6 +156,42 @@ async def async_joinall(tasks):
154156
await asyncio.wait([t.task for t in tasks if t is not None], timeout=300)
155157

156158

159+
def flaky(func=None, *, max_runs=2, min_passes=1, delay=1, affects_cpython_linux=False):
160+
is_cpython_linux = sys.platform == "linux" and sys.implementation.name == "cpython"
161+
if is_cpython_linux and not affects_cpython_linux:
162+
max_runs = 1
163+
min_passes = 1
164+
165+
def decorator(target_func):
166+
@wraps(target_func)
167+
async def wrapper(*args, **kwargs):
168+
passes = 0
169+
failure = None
170+
for i in range(max_runs):
171+
try:
172+
result = await target_func(*args, **kwargs)
173+
passes += 1
174+
if passes == min_passes:
175+
return result
176+
except Exception as e:
177+
failure = e
178+
await asyncio.sleep(delay)
179+
if failure is not None and i < max_runs - 1:
180+
print("Flaky failure:", failure)
181+
if failure:
182+
raise failure
183+
raise RuntimeError(f"Only passed {passes} of {min_passes} times")
184+
185+
return wrapper
186+
187+
# If `func` is callable, the decorator was used without arguments (`@flaky`)
188+
if callable(func):
189+
return decorator(func)
190+
191+
# Otherwise, return the decorator function, allowing arguments (`@flaky(max_runs=...)`)
192+
return decorator
193+
194+
157195
class AsyncMockConnection:
158196
def __init__(self):
159197
self.cancel_context = _CancellationContext()

test/test_client_bulk_write.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
client_context,
2626
unittest,
2727
)
28+
from test.utils import flaky
2829
from test.utils_shared import (
2930
OvertCommandListener,
3031
)
@@ -615,15 +616,14 @@ def test_15_unacknowledged_write_across_batches(self):
615616
# https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#11-multi-batch-bulkwrites
616617
class TestClientBulkWriteCSOT(IntegrationTest):
617618
def setUp(self):
618-
if os.environ.get("SKIP_CSOT_TESTS", ""):
619-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
620619
super().setUp()
621620
self.max_write_batch_size = client_context.max_write_batch_size
622621
self.max_bson_object_size = client_context.max_bson_size
623622
self.max_message_size_bytes = client_context.max_message_size_bytes
624623

625624
@client_context.require_version_min(8, 0, 0, -24)
626625
@client_context.require_failCommand_fail_point
626+
@flaky
627627
def test_timeout_in_multi_batch_bulk_write(self):
628628
_OVERHEAD = 500
629629

test/test_csot.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from test import IntegrationTest, client_context, unittest
2525
from test.unified_format import generate_test_classes
26+
from test.utils import flaky
2627

2728
import pymongo
2829
from pymongo import _csot
@@ -43,9 +44,8 @@
4344
class TestCSOT(IntegrationTest):
4445
RUN_ON_LOAD_BALANCER = True
4546

47+
@flaky
4648
def test_timeout_nested(self):
47-
if os.environ.get("SKIP_CSOT_TESTS", ""):
48-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
4949
coll = self.db.coll
5050
self.assertEqual(_csot.get_timeout(), None)
5151
self.assertEqual(_csot.get_deadline(), float("inf"))
@@ -82,9 +82,8 @@ def test_timeout_nested(self):
8282
self.assertEqual(_csot.get_rtt(), 0.0)
8383

8484
@client_context.require_change_streams
85+
@flaky
8586
def test_change_stream_can_resume_after_timeouts(self):
86-
if os.environ.get("SKIP_CSOT_TESTS", ""):
87-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
8887
coll = self.db.test
8988
coll.insert_one({})
9089
with coll.watch() as stream:

test/test_cursor.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
sys.path[0:0] = [""]
3232

3333
from test import IntegrationTest, client_context, unittest
34+
from test.utils import flaky
3435
from test.utils_shared import (
3536
AllowListEventListener,
3637
EventListener,
@@ -1406,9 +1407,8 @@ def test_to_list_length(self):
14061407
docs = c.to_list(3)
14071408
self.assertEqual(len(docs), 2)
14081409

1410+
@flaky
14091411
def test_to_list_csot_applied(self):
1410-
if os.environ.get("SKIP_CSOT_TESTS", ""):
1411-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
14121412
client = self.single_client(timeoutMS=500, w=1)
14131413
coll = client.pymongo.test
14141414
# Initialize the client with a larger timeout to help make test less flakey
@@ -1449,9 +1449,8 @@ def test_command_cursor_to_list_length(self):
14491449
self.assertEqual(len(result.to_list(1)), 1)
14501450

14511451
@client_context.require_failCommand_blockConnection
1452+
@flaky
14521453
def test_command_cursor_to_list_csot_applied(self):
1453-
if os.environ.get("SKIP_CSOT_TESTS", ""):
1454-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
14551454
client = self.single_client(timeoutMS=500, w=1)
14561455
coll = client.pymongo.test
14571456
# Initialize the client with a larger timeout to help make test less flakey

test/unified_format.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
from test.unified_format_shared import (
3939
KMS_TLS_OPTS,
4040
PLACEHOLDER_MAP,
41-
SKIP_CSOT_TESTS,
4241
EventListenerUtil,
4342
MatchEvaluatorUtil,
4443
coerce_result,
@@ -1361,9 +1360,6 @@ def verify_outcome(self, spec):
13611360
self.assertListEqual(sorted_expected_documents, actual_documents)
13621361

13631362
def run_scenario(self, spec, uri=None):
1364-
if "csot" in self.id().lower() and SKIP_CSOT_TESTS:
1365-
raise unittest.SkipTest("SKIP_CSOT_TESTS is set, skipping...")
1366-
13671363
# Kill all sessions before and after each test to prevent an open
13681364
# transaction (from a test failure) from blocking collection/database
13691365
# operations during test set up and tear down.

0 commit comments

Comments
 (0)