Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d01078c
Fix flaky import time test for Python 3.12+
Jan 24, 2026
3597661
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 24, 2026
a6313f9
Fix flaky test_regex_performance timing test
Jan 24, 2026
6ec38c6
Improve flaky test handling using pytest-rerunfailures
Jan 25, 2026
7b58495
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 25, 2026
afea84b
Fix socket leaks in TestShutdown suite for Windows CI
Jan 26, 2026
55e7368
reverting the windows socket handling. The scope might be growing too…
Jan 26, 2026
c81897f
Refactor get_flaky_threshold into rerun_adjusted_threshold fixture
Jan 26, 2026
4019cac
Marking flaky tests to rerun
Jan 26, 2026
a0ba5e4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
9c60f37
Fix make_client_request fixture to prevent session leaks
Jan 28, 2026
0b181b6
fix: add Windows cleanup delay in secure_proxy_url fixture
Jan 28, 2026
e5efc52
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 28, 2026
1e8374a
fix: increase Windows cleanup delay to 0.5s with multiple gc passes
Jan 28, 2026
6c1a4b1
test: use extreme 5s delay to verify socket leak source
Jan 28, 2026
e55b246
test: add delay after gc.collect() to test async finalization
Jan 28, 2026
e99fea2
test: use thread polling instead of fixed sleep for Windows cleanup
Jan 29, 2026
1787cdd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 29, 2026
f3c5e29
test: use baseline thread detection for Windows cleanup
Jan 29, 2026
859da2d
fix: Improve thread cleanup in proxy test fixture by simplifying thre…
Jan 29, 2026
187b072
Add Windows socket warning filter for Py3.10/3.11
Jan 30, 2026
8e33e20
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 30, 2026
dbea725
refactor: introduce RerunThresholdParams NamedTuple for performance t…
Jan 30, 2026
114fc76
refactor: use asyncio.gather() for parallel cleanup in make_request f…
Jan 30, 2026
a07489e
Remove dynamic thresholds and use fixed values for performance tests
Feb 7, 2026
f781696
fix: suppress unraisable exception warnings on Windows for Python 3.1…
Feb 12, 2026
e5b3c1e
Update tests/conftest.py
rodrigobnogueira Feb 14, 2026
e2b107f
Inline regex/threshold constants & restore test_import_time best-of-3
Feb 14, 2026
2752514
Fix flaky test_uvloop_secure_https_proxy: use local server instead of…
Feb 14, 2026
cbc3021
refactor: move `best_time_ms` initialization after `PYTHONPATH` setup…
Feb 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/11992.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed flaky performance tests by using appropriate fixed thresholds that account for CI variability -- by :user:`rodrigobnogueira`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ Raúl Cumplido
Required Field
Robert Lu
Robert Nikolich
Rodrigo Nogueira
Roman Markeloff
Roman Podoliaka
Roman Postnov
Expand Down
26 changes: 20 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@
TRUSTME = False


def pytest_configure(config: pytest.Config) -> None:
# On Windows with Python 3.10/3.11, proxy.py's threaded mode can leave
# sockets not fully released by the time pytest's unraisableexception
# plugin collects warnings during teardown. Suppress these warnings
# since they are not actionable and only affect older Python versions.
if os.name == "nt" and sys.version_info[:2] in ((3, 10), (3, 11)):
config.addinivalue_line(
"filterwarnings",
"ignore:Exception ignored in.*socket.*:pytest.PytestUnraisableExceptionWarning",
)


try:
if sys.platform == "win32":
import winloop as uvloop
Expand Down Expand Up @@ -431,13 +443,14 @@ async def make_client_request(
loop: asyncio.AbstractEventLoop,
) -> AsyncIterator[Callable[[str, URL, Unpack[ClientRequestArgs]], ClientRequest]]:
"""Fixture to help creating test ClientRequest objects with defaults."""
request = session = None
requests: list[ClientRequest] = []
sessions: list[ClientSession] = []

def maker(
method: str, url: URL, **kwargs: Unpack[ClientRequestArgs]
) -> ClientRequest:
nonlocal request, session
session = ClientSession()
sessions.append(session)
default_args: ClientRequestArgs = {
"loop": loop,
"params": {},
Expand All @@ -462,14 +475,15 @@ def maker(
"server_hostname": None,
}
request = ClientRequest(method, url, **(default_args | kwargs))
requests.append(request)
return request

yield maker

if request is not None:
await request._close()
assert session is not None
await session.close()
await asyncio.gather(
*(request._close() for request in requests),
*(session.close() for session in sessions),
)


@pytest.fixture
Expand Down
16 changes: 12 additions & 4 deletions tests/test_client_middleware_digest_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1331,13 +1331,21 @@ async def handler(request: Request) -> Response:
assert auth_algorithms[0] == "MD5-sess" # Not "MD5-SESS"


_REGEX_TIME_THRESHOLD_SECONDS = 0.08


def test_regex_performance() -> None:
"""Test that the regex pattern doesn't suffer from ReDoS issues."""
value = "0" * 54773 + "\\0=a"

start = time.perf_counter()
matches = _HEADER_PAIRS_PATTERN.findall(value)
end = time.perf_counter()
elapsed = time.perf_counter() - start

# If this is taking more than 10ms, there's probably a performance/ReDoS issue.
assert (end - start) < 0.01
# This example probably shouldn't produce a match either.
# If this is taking more time, there's probably a performance/ReDoS issue.
assert elapsed < _REGEX_TIME_THRESHOLD_SECONDS, (
f"Regex took {elapsed * 1000:.1f}ms, "
f"expected <{_REGEX_TIME_THRESHOLD_SECONDS * 1000:.0f}ms - potential ReDoS issue"
)
# This example shouldn't produce a match either.
assert not matches
13 changes: 10 additions & 3 deletions tests/test_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,14 +637,21 @@ def test_cookie_pattern_matches_partitioned_attribute(test_string: str) -> None:
assert match.group("key").lower() == "partitioned"


_COOKIE_PATTERN_TIME_THRESHOLD_SECONDS = 0.08


def test_cookie_pattern_performance() -> None:
"""Test that the cookie pattern doesn't suffer from ReDoS issues."""
value = "a" + "=" * 21651 + "\x00"
start = time.perf_counter()
match = helpers._COOKIE_PATTERN.match(value)
end = time.perf_counter()
elapsed = time.perf_counter() - start

# If this is taking more than 10ms, there's probably a performance/ReDoS issue.
assert (end - start) < 0.01
# If this is taking more time, there's probably a performance/ReDoS issue.
assert elapsed < _COOKIE_PATTERN_TIME_THRESHOLD_SECONDS, (
f"Pattern took {elapsed * 1000:.1f}ms, "
f"expected <{_COOKIE_PATTERN_TIME_THRESHOLD_SECONDS * 1000:.0f}ms - potential ReDoS issue"
)
# This example shouldn't produce a match either.
assert match is None

Expand Down
31 changes: 5 additions & 26 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,7 @@ def test_web___all__(pytester: pytest.Pytester) -> None:
result.assert_outcomes(passed=0, errors=0)


_IS_CI_ENV = os.getenv("CI") == "true"
_XDIST_WORKER_COUNT = int(os.getenv("PYTEST_XDIST_WORKER_COUNT", 0))
_IS_XDIST_RUN = _XDIST_WORKER_COUNT > 1

_TARGET_TIMINGS_BY_PYTHON_VERSION = {
"3.12": (
# 3.12+ is expected to be a bit slower due to performance trade-offs,
# and even slower under pytest-xdist, especially in CI
_XDIST_WORKER_COUNT * 100 * (1 if _IS_CI_ENV else 1.53)
if _IS_XDIST_RUN
else 295
),
}
_TARGET_TIMINGS_BY_PYTHON_VERSION["3.13"] = _TARGET_TIMINGS_BY_PYTHON_VERSION["3.12"]
_IMPORT_TIME_THRESHOLD_MS = 400 if sys.version_info >= (3, 12) else 300


@pytest.mark.internal
Expand All @@ -61,23 +48,15 @@ def test_import_time(pytester: pytest.Pytester) -> None:
old_path = os.environ.get("PYTHONPATH")
os.environ["PYTHONPATH"] = os.pathsep.join([str(root)] + sys.path)

best_time_ms = 1000
cmd = "import timeit; print(int(timeit.timeit('import aiohttp', number=1) * 1000))"
try:
for _ in range(3):
r = pytester.run(sys.executable, "-We", "-c", cmd)

assert not r.stderr.str()
runtime_ms = int(r.stdout.str())
if runtime_ms < best_time_ms:
best_time_ms = runtime_ms
r = pytester.run(sys.executable, "-We", "-c", cmd)
assert not r.stderr.str(), r.stderr.str()
runtime_ms = int(r.stdout.str())
finally:
if old_path is None:
os.environ.pop("PYTHONPATH")
else:
os.environ["PYTHONPATH"] = old_path

expected_time = _TARGET_TIMINGS_BY_PYTHON_VERSION.get(
f"{sys.version_info.major}.{sys.version_info.minor}", 200
)
assert best_time_ms < expected_time
assert runtime_ms < _IMPORT_TIME_THRESHOLD_MS
12 changes: 9 additions & 3 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,14 +600,20 @@ def test_single_forwarded_header() -> None:
assert req.forwarded[0]["proto"] == "identifier"


_FORWARDED_RE_TIME_THRESHOLD_SECONDS = 0.08


def test_forwarded_re_performance() -> None:
value = "{" + "f" * 54773 + "z\x00a=v"
start = time.perf_counter()
match = _FORWARDED_PAIR_RE.match(value)
end = time.perf_counter()
elapsed = time.perf_counter() - start

# If this is taking more than 10ms, there's probably a performance/ReDoS issue.
assert (end - start) < 0.01
# If this is taking more time, there's probably a performance/ReDoS issue.
assert elapsed < _FORWARDED_RE_TIME_THRESHOLD_SECONDS, (
f"Regex took {elapsed * 1000:.1f}ms, "
f"expected <{_FORWARDED_RE_TIME_THRESHOLD_SECONDS * 1000:.0f}ms - potential ReDoS issue"
)
# This example shouldn't produce a match either.
assert match is None

Expand Down
Loading