Skip to content

Commit 1eed494

Browse files
author
rodrigo.nogueira
committed
Improve flaky test handling using pytest-rerunfailures
1 parent 553f63e commit 1eed494

4 files changed

Lines changed: 61 additions & 35 deletions

File tree

aiohttp/pytest_plugin.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ def __call__(
6464
) -> Awaitable[RawTestServer]: ...
6565

6666

67+
def get_flaky_threshold(
68+
request: pytest.FixtureRequest,
69+
base: float,
70+
increment: float,
71+
) -> float:
72+
"""Calculate dynamic threshold for flaky tests based on rerun count.
73+
74+
When using `@pytest.mark.flaky(reruns=N)`:
75+
- execution_count is 1-based (1 for first run, 2 for first rerun, etc.)
76+
- With reruns=3, the test runs up to 4 times (1 initial + 3 reruns)
77+
- Returns base threshold on first run, incrementing by `increment` per rerun
78+
79+
Example with reruns=3, base=20ms, increment=30ms:
80+
Run 1: 20ms, Rerun 1: 50ms, Rerun 2: 80ms, Rerun 3: 110ms
81+
"""
82+
execution_count: int = getattr(request.node, "execution_count", 0)
83+
rerun_count = max(0, execution_count - 1)
84+
return base + (rerun_count * increment)
85+
86+
6787
def pytest_addoption(parser): # type: ignore[no-untyped-def]
6888
parser.addoption(
6989
"--aiohttp-fast",

requirements/test-common.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5
88
pytest
99
pytest-cov
1010
pytest-mock
11+
pytest-rerunfailures
1112
pytest-xdist
1213
pytest_codspeed
1314
python-on-whales

tests/test_client_middleware_digest_auth.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from aiohttp.payload import BytesIOPayload
2727
from aiohttp.pytest_plugin import AiohttpServer
2828
from aiohttp.web import Application, Request, Response
29+
from aiohttp.pytest_plugin import get_flaky_threshold
2930

3031

3132
@pytest.fixture
@@ -1331,25 +1332,27 @@ async def handler(request: Request) -> Response:
13311332
assert auth_algorithms[0] == "MD5-sess" # Not "MD5-SESS"
13321333

13331334

1334-
def test_regex_performance() -> None:
1335-
value = "0" * 54773 + "\\0=a"
1335+
@pytest.mark.flaky(reruns=3)
1336+
def test_regex_performance(request: pytest.FixtureRequest) -> None:
1337+
"""Test that the regex pattern doesn't suffer from ReDoS issues.
13361338
1337-
best_time = float("inf")
1338-
best_matches: list[tuple[str, str]] = []
1339+
Threshold starts at 20ms and increases on each rerun for CI variability.
1340+
"""
1341+
REGEX_TIME_THRESHOLD_DEFAULT = 0.02 # 20ms
1342+
REGEX_TIME_INCREMENT_PER_RERUN = 0.03 # 30ms
1343+
# CI/platform variability (e.g., macOS runners ~40-50ms observed)
1344+
threshold_ms = get_flaky_threshold(
1345+
request, REGEX_TIME_THRESHOLD_DEFAULT, REGEX_TIME_INCREMENT_PER_RERUN
1346+
)
13391347

1340-
for _ in range(5):
1341-
start = time.perf_counter()
1342-
matches = _HEADER_PAIRS_PATTERN.findall(value)
1343-
elapsed = time.perf_counter() - start
1348+
value = "0" * 54773 + "\\0=a"
13441349

1345-
if elapsed < best_time:
1346-
best_time = elapsed
1347-
best_matches = matches
1350+
start = time.perf_counter()
1351+
matches = _HEADER_PAIRS_PATTERN.findall(value)
1352+
elapsed = time.perf_counter() - start
13481353

1349-
# Relaxed for CI/platform variability (e.g., macOS runners ~40-50ms observed)
13501354
assert (
1351-
best_time < 0.1
1352-
), f"Regex took {best_time * 1000:.1f}ms, expected <100ms - potential ReDoS issue"
1355+
elapsed < threshold_ms
1356+
), f"Regex took {elapsed * 1000:.1f}ms, expected <{threshold_ms * 1000:.0f}ms - potential ReDoS issue"
13531357

1354-
# This example probably shouldn't produce a match either.
1355-
assert not best_matches
1358+
assert not matches

tests/test_imports.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import platform
33
import sys
44
from pathlib import Path
5-
65
import pytest
6+
from aiohttp.pytest_plugin import get_flaky_threshold
77

88

99
def test___all__(pytester: pytest.Pytester) -> None:
@@ -28,48 +28,50 @@ def test_web___all__(pytester: pytest.Pytester) -> None:
2828
result.assert_outcomes(passed=0, errors=0)
2929

3030

31-
_IMPORT_TIME_THRESHOLD_PY312 = 350
31+
_IMPORT_TIME_THRESHOLD_PY312 = 300
3232
_IMPORT_TIME_THRESHOLD_DEFAULT = 200
33-
34-
35-
def _get_import_time_threshold() -> float:
36-
if sys.version_info >= (3, 12):
37-
return _IMPORT_TIME_THRESHOLD_PY312
38-
return _IMPORT_TIME_THRESHOLD_DEFAULT
33+
_IMPORT_TIME_INCREMENT_PER_RERUN = 50
3934

4035

4136
@pytest.mark.internal
4237
@pytest.mark.dev_mode
38+
@pytest.mark.flaky(reruns=3)
4339
@pytest.mark.skipif(
4440
not sys.platform.startswith("linux") or platform.python_implementation() == "PyPy",
4541
reason="Timing is more reliable on Linux",
4642
)
47-
def test_import_time(pytester: pytest.Pytester) -> None:
43+
def test_import_time(request: pytest.FixtureRequest, pytester: pytest.Pytester) -> None:
4844
"""Check that importing aiohttp doesn't take too long.
4945
5046
Obviously, the time may vary on different machines and may need to be adjusted
5147
from time to time, but this should provide an early warning if something is
5248
added that significantly increases import time.
49+
50+
Threshold increases by _IMPORT_TIME_INCREMENT_PER_RERUN ms on each rerun
51+
to account for CI variability.
5352
"""
53+
base_threshold = (
54+
_IMPORT_TIME_THRESHOLD_PY312
55+
if sys.version_info >= (3, 12)
56+
else _IMPORT_TIME_THRESHOLD_DEFAULT
57+
)
58+
expected_time = get_flaky_threshold(
59+
request, base_threshold, _IMPORT_TIME_INCREMENT_PER_RERUN
60+
)
61+
5462
root = Path(__file__).parent.parent
5563
old_path = os.environ.get("PYTHONPATH")
5664
os.environ["PYTHONPATH"] = os.pathsep.join([str(root)] + sys.path)
5765

58-
best_time_ms = 1000
5966
cmd = "import timeit; print(int(timeit.timeit('import aiohttp', number=1) * 1000))"
6067
try:
61-
for _ in range(3):
62-
r = pytester.run(sys.executable, "-We", "-c", cmd)
63-
64-
assert not r.stderr.str(), r.stderr.str()
65-
runtime_ms = int(r.stdout.str())
66-
if runtime_ms < best_time_ms:
67-
best_time_ms = runtime_ms
68+
r = pytester.run(sys.executable, "-We", "-c", cmd)
69+
assert not r.stderr.str(), r.stderr.str()
70+
runtime_ms = int(r.stdout.str())
6871
finally:
6972
if old_path is None:
7073
os.environ.pop("PYTHONPATH")
7174
else:
7275
os.environ["PYTHONPATH"] = old_path
7376

74-
expected_time = _get_import_time_threshold()
75-
assert best_time_ms < expected_time
77+
assert runtime_ms < expected_time

0 commit comments

Comments
 (0)