Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cf37399
Fix flaky import time test for Python 3.12+
Jan 24, 2026
877749b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 24, 2026
553f63e
Fix flaky test_regex_performance timing test
Jan 24, 2026
1eed494
Improve flaky test handling using pytest-rerunfailures
Jan 25, 2026
1e0e7b5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 25, 2026
c1f3847
Fix socket leaks in TestShutdown suite for Windows CI
Jan 26, 2026
5bc9395
reverting the windows socket handling. The scope might be growing too…
Jan 26, 2026
6eaa28b
Merge branch 'master' into fix/import-time-test-python-3.14
rodrigobnogueira Jan 26, 2026
defe900
Regenerate test requirement pins to include pytest-rerunfailures
Jan 26, 2026
2a39d88
Refactor get_flaky_threshold into rerun_adjusted_threshold fixture
Jan 26, 2026
7c338ff
Marking flaky tests to rerun
Jan 26, 2026
099fe5f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 26, 2026
4a2e2e0
Merge upstream/master and resolve requirements conflicts
Jan 28, 2026
d10cd20
Fix make_client_request fixture to prevent session leaks
Jan 28, 2026
8aa32b7
fix: add Windows cleanup delay in secure_proxy_url fixture
Jan 28, 2026
00d8bab
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 28, 2026
85ffe07
fix: increase Windows cleanup delay to 0.5s with multiple gc passes
Jan 28, 2026
f905bc7
test: use extreme 5s delay to verify socket leak source
Jan 28, 2026
f6a3a00
test: add delay after gc.collect() to test async finalization
Jan 28, 2026
7315a1b
test: use thread polling instead of fixed sleep for Windows cleanup
Jan 29, 2026
968d9bd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 29, 2026
e63fb6d
test: use baseline thread detection for Windows cleanup
Jan 29, 2026
4d75c6a
fix: Improve thread cleanup in proxy test fixture by simplifying thre…
Jan 29, 2026
ac1cb5b
Add Windows socket warning filter for Py3.10/3.11
Jan 30, 2026
b8aee4a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 30, 2026
ab00b7b
refactor: introduce RerunThresholdParams NamedTuple for performance t…
Jan 30, 2026
0f3fb76
refactor: use asyncio.gather() for parallel cleanup in make_request f…
Jan 30, 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
4 changes: 4 additions & 0 deletions CHANGES/11992.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed flaky import time test for Python 3.12+ -- by :user:`rodrigobnogueira`.

Refactored to use version comparison instead of explicit version list,
making the test future-proof for new Python releases.
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
33 changes: 32 additions & 1 deletion aiohttp/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import inspect
import warnings
from collections.abc import Awaitable, Callable, Iterator
from typing import Any, Protocol, TypeVar, overload
from typing import Any, NamedTuple, Protocol, TypeVar, overload

import pytest

Expand Down Expand Up @@ -64,6 +64,37 @@ def __call__(
) -> Awaitable[RawTestServer]: ...


class RerunThresholdParams(NamedTuple):
"""Parameters for dynamic threshold calculation in flaky tests."""

base: float
increment_per_rerun: float


@pytest.fixture
def rerun_adjusted_threshold(request: pytest.FixtureRequest) -> float:
"""Calculate dynamic threshold based on rerun count (via indirect parametrization).

Returns ``base + (rerun_count * increment_per_rerun)``.
The ``rerun_count`` is determined from ``pytest-rerunfailures`` (0 for initial run,
1 for first rerun, etc.).

Usage::

@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize(
"rerun_adjusted_threshold",
[RerunThresholdParams(base=0.02, increment_per_rerun=0.02)],
indirect=True,
)
def test_timing(rerun_adjusted_threshold: float) -> None: ...
"""
param: RerunThresholdParams = request.param
execution_count: int = getattr(request.node, "execution_count", 0)
rerun_count = max(0, execution_count - 1)
return param.base + (rerun_count * param.increment_per_rerun)


def pytest_addoption(parser): # type: ignore[no-untyped-def]
parser.addoption(
"--aiohttp-fast",
Expand Down
1 change: 1 addition & 0 deletions requirements/test-common.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5
pytest
pytest-cov
pytest-mock
pytest-rerunfailures
pytest-xdist
pytest_codspeed
python-on-whales
Expand Down
3 changes: 3 additions & 0 deletions requirements/test-common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,16 @@ pytest==9.0.2
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-rerunfailures
# pytest-xdist
pytest-codspeed==4.2.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
pytest-rerunfailures==16.1
# via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
Expand Down
3 changes: 3 additions & 0 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,16 @@ pytest==9.0.2
# pytest-codspeed
# pytest-cov
# pytest-mock
# pytest-rerunfailures
# pytest-xdist
pytest-codspeed==4.2.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
pytest-rerunfailures==16.1
# via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
Expand Down
22 changes: 16 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
TRUSTME = False


def pytest_configure(config: pytest.Config) -> None:
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 +439,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 +471,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
27 changes: 21 additions & 6 deletions tests/test_client_middleware_digest_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
)
from aiohttp.client_reqrep import ClientResponse
from aiohttp.payload import BytesIOPayload
from aiohttp.pytest_plugin import AiohttpServer
from aiohttp.pytest_plugin import AiohttpServer, RerunThresholdParams
from aiohttp.web import Application, Request, Response


Expand Down Expand Up @@ -1331,13 +1331,28 @@ async def handler(request: Request) -> Response:
assert auth_algorithms[0] == "MD5-sess" # Not "MD5-SESS"


def test_regex_performance() -> None:
_REGEX_TIME_THRESHOLD = RerunThresholdParams(base=0.02, increment_per_rerun=0.02)


@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize(
"rerun_adjusted_threshold", [_REGEX_TIME_THRESHOLD], indirect=True
)
def test_regex_performance(rerun_adjusted_threshold: float) -> None:
"""Test that the regex pattern doesn't suffer from ReDoS issues.

Threshold starts at 20ms and increases on each rerun for CI variability.
"""
value = "0" * 54773 + "\\0=a"

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

# Relaxed for CI/platform variability (e.g., macOS runners ~40-50ms observed)
assert elapsed < rerun_adjusted_threshold, (
f"Regex took {elapsed * 1000:.1f}ms, "
f"expected <{rerun_adjusted_threshold * 1000:.0f}ms - potential ReDoS issue"
)

# 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.
assert not matches
26 changes: 21 additions & 5 deletions tests/test_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
parse_set_cookie_headers,
preserve_morsel_with_coded_value,
)
from aiohttp.pytest_plugin import RerunThresholdParams


def test_known_attrs_is_superset_of_morsel_reserved() -> None:
Expand Down Expand Up @@ -637,15 +638,30 @@ def test_cookie_pattern_matches_partitioned_attribute(test_string: str) -> None:
assert match.group("key").lower() == "partitioned"


def test_cookie_pattern_performance() -> None:
_COOKIE_PATTERN_TIME_THRESHOLD = RerunThresholdParams(
base=0.02, increment_per_rerun=0.02
)


@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize(
"rerun_adjusted_threshold", [_COOKIE_PATTERN_TIME_THRESHOLD], indirect=True
)
def test_cookie_pattern_performance(rerun_adjusted_threshold: float) -> None:
"""Test that the cookie pattern doesn't suffer from ReDoS issues.

This test is marked as flaky because timing can vary on loaded CI machines.
CI failure observed: ~20ms on Windows.
"""
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
# This example shouldn't produce a match either.
assert elapsed < rerun_adjusted_threshold, (
f"Pattern took {elapsed * 1000:.1f}ms, "
f"expected <{rerun_adjusted_threshold * 1000:.0f}ms - potential ReDoS issue"
)
assert match is None


Expand Down
52 changes: 26 additions & 26 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import pytest

from aiohttp.pytest_plugin import RerunThresholdParams


def test___all__(pytester: pytest.Pytester) -> None:
"""See https://github.com/aio-libs/aiohttp/issues/6197"""
Expand All @@ -28,56 +30,54 @@ 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
_IMPORT_TIME_THRESHOLD_PY312 = 300
_IMPORT_TIME_THRESHOLD_DEFAULT = 200
_IMPORT_TIME_INCREMENT_PER_RERUN = 50
_IMPORT_TIME_THRESHOLD = RerunThresholdParams(
base=(
_IMPORT_TIME_THRESHOLD_PY312
if sys.version_info >= (3, 12)
else _IMPORT_TIME_THRESHOLD_DEFAULT
),
}
_TARGET_TIMINGS_BY_PYTHON_VERSION["3.13"] = _TARGET_TIMINGS_BY_PYTHON_VERSION["3.12"]
increment_per_rerun=_IMPORT_TIME_INCREMENT_PER_RERUN,
)


@pytest.mark.internal
@pytest.mark.dev_mode
@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize(
"rerun_adjusted_threshold", [_IMPORT_TIME_THRESHOLD], indirect=True
)
@pytest.mark.skipif(
not sys.platform.startswith("linux") or platform.python_implementation() == "PyPy",
reason="Timing is more reliable on Linux",
)
def test_import_time(pytester: pytest.Pytester) -> None:
def test_import_time(
rerun_adjusted_threshold: float, pytester: pytest.Pytester
) -> None:
"""Check that importing aiohttp doesn't take too long.

Obviously, the time may vary on different machines and may need to be adjusted
from time to time, but this should provide an early warning if something is
added that significantly increases import time.

Threshold increases by _IMPORT_TIME_INCREMENT_PER_RERUN ms on each rerun
to account for CI variability.
"""
root = Path(__file__).parent.parent
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 < rerun_adjusted_threshold
20 changes: 14 additions & 6 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from aiohttp import ETag, HttpVersion, web
from aiohttp.base_protocol import BaseProtocol
from aiohttp.http_parser import RawRequestMessage
from aiohttp.pytest_plugin import AiohttpClient
from aiohttp.pytest_plugin import AiohttpClient, RerunThresholdParams
from aiohttp.streams import StreamReader
from aiohttp.test_utils import make_mocked_request
from aiohttp.web_request import _FORWARDED_PAIR_RE
Expand Down Expand Up @@ -600,15 +600,23 @@ def test_single_forwarded_header() -> None:
assert req.forwarded[0]["proto"] == "identifier"


def test_forwarded_re_performance() -> None:
_FORWARDED_RE_TIME_THRESHOLD = RerunThresholdParams(base=0.02, increment_per_rerun=0.02)


@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize(
"rerun_adjusted_threshold", [_FORWARDED_RE_TIME_THRESHOLD], indirect=True
)
def test_forwarded_re_performance(rerun_adjusted_threshold: float) -> 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
# This example shouldn't produce a match either.
assert elapsed < rerun_adjusted_threshold, (
f"Regex took {elapsed * 1000:.1f}ms, "
f"expected <{rerun_adjusted_threshold * 1000:.0f}ms - potential ReDoS issue"
)
assert match is None


Expand Down
Loading