Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 < (3, 12):
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
14 changes: 10 additions & 4 deletions tests/test_client_middleware_digest_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,12 +1332,18 @@ async def handler(request: Request) -> Response:


def test_regex_performance() -> None:
"""Test that the regex pattern doesn't suffer from ReDoS issues."""
REGEX_TIME_THRESHOLD_SECONDS = 0.08
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
11 changes: 8 additions & 3 deletions tests/test_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,13 +638,18 @@ def test_cookie_pattern_matches_partitioned_attribute(test_string: str) -> None:


def test_cookie_pattern_performance() -> None:
"""Test that the cookie pattern doesn't suffer from ReDoS issues."""
COOKIE_PATTERN_TIME_THRESHOLD_SECONDS = 0.08
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: 6 additions & 25 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,6 @@ 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"]


@pytest.mark.internal
@pytest.mark.dev_mode
@pytest.mark.skipif(
Expand All @@ -56,7 +40,10 @@ def test_import_time(pytester: pytest.Pytester) -> None:
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.

Runs 3 times and keeps the minimum time to reduce flakiness.
"""
IMPORT_TIME_THRESHOLD_MS = 300 if sys.version_info >= (3, 12) else 200
root = Path(__file__).parent.parent
old_path = os.environ.get("PYTHONPATH")
os.environ["PYTHONPATH"] = os.pathsep.join([str(root)] + sys.path)
Expand All @@ -66,18 +53,12 @@ def test_import_time(pytester: pytest.Pytester) -> None:
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
assert not r.stderr.str(), r.stderr.str()
best_time_ms = min(best_time_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 best_time_ms < IMPORT_TIME_THRESHOLD_MS
19 changes: 15 additions & 4 deletions tests/test_proxy_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from aiohttp.client import _RequestOptions
from aiohttp.client_exceptions import ClientConnectionError
from aiohttp.pytest_plugin import AiohttpRawServer, AiohttpServer
from aiohttp.test_utils import TestServer

ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11)

Expand Down Expand Up @@ -244,24 +245,34 @@ async def test_https_proxy_unsupported_tls_in_tls(
# otherwise this test will fail because the proxy will die with an error.
async def test_uvloop_secure_https_proxy(
client_ssl_ctx: ssl.SSLContext,
ssl_ctx: ssl.SSLContext,
secure_proxy_url: URL,
uvloop_loop: asyncio.AbstractEventLoop,
) -> None:
"""Ensure HTTPS sites are accessible through a secure proxy without warning when using uvloop."""
payload = str(uuid4())

async def handler(request: web.Request) -> web.Response:
return web.Response(text=payload)

app = web.Application()
app.router.add_route("GET", "/", handler)
server = TestServer(app, host="127.0.0.1")
await server.start_server(ssl=ssl_ctx)

url = URL.build(scheme="https", host=server.host, port=server.port)
conn = aiohttp.TCPConnector(force_close=True)
sess = aiohttp.ClientSession(connector=conn)
try:
url = URL("https://example.com")

async with sess.get(
url, proxy=secure_proxy_url, ssl=client_ssl_ctx
) as response:
assert response.status == 200
# Ensure response body is read to completion
await response.read()
assert await response.text() == payload
finally:
await sess.close()
await conn.close()
await server.close()
await asyncio.sleep(0)
await asyncio.sleep(0.1)

Expand Down
10 changes: 7 additions & 3 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,13 +601,17 @@ def test_single_forwarded_header() -> None:


def test_forwarded_re_performance() -> None:
FORWARDED_RE_TIME_THRESHOLD_SECONDS = 0.08
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