diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b1bde397283..9a366681b87 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -182,6 +182,7 @@ jobs: fail-fast: true runs-on: ${{ matrix.os }}-latest continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 @@ -294,6 +295,7 @@ jobs: os: [ubuntu] fail-fast: true runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 @@ -346,7 +348,7 @@ jobs: PIP_USER: 1 run: >- PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}" - pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ -m autobahn + pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ --timeout=0 -m autobahn shell: bash - name: Turn coverage into xml env: @@ -409,7 +411,7 @@ jobs: uses: CodSpeedHQ/action@v4 with: mode: instrumentation - run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 + run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 --timeout=0 cython-coverage: @@ -422,6 +424,7 @@ jobs: matrix: os: [ubuntu, windows] runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 diff --git a/.gitignore b/.gitignore index 0081b62ae7f..ee06baff610 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ sources var/* venv virtualenv.py + +# Claude Code per-checkout overrides +CLAUDE.local.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12f2c8678e9..ce55819c793 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -64,7 +64,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: '26.3.1' + rev: '26.5.0' hooks: - id: black language_version: python3 # Should be a command that runs python diff --git a/CHANGES/12606.contrib.rst b/CHANGES/12606.contrib.rst new file mode 100644 index 00000000000..72925114e8c --- /dev/null +++ b/CHANGES/12606.contrib.rst @@ -0,0 +1,6 @@ +Reduced runtime of several of the slowest unit tests +(decompress size-limit payloads from 64 MiB to 2 MiB, +``test_chunk_splits_after_pause`` chunk count from 50000 +to 20000, and ``test_set_cookies_max_age`` sleep from 2 +seconds to 1.1 seconds) without changing what they +exercise -- by :user:`bdraco`. diff --git a/CHANGES/12617.contrib.rst b/CHANGES/12617.contrib.rst new file mode 100644 index 00000000000..fab91aea8e0 --- /dev/null +++ b/CHANGES/12617.contrib.rst @@ -0,0 +1,9 @@ +Restructured the root :file:`CLAUDE.md` to import contributor +context from a shared ``aio-libs`` layer +(``~/.claude/aio-libs/context.md``), a project-specific layer +(``~/.claude/aio-libs/aiohttp/context.md``), the in-tree +:file:`AGENTS.md`, and an optional per-checkout +:file:`CLAUDE.local.md` override (added to :file:`.gitignore`), +so shared ``aio-libs`` guidance can live outside the repository +while project rules continue to load automatically in Claude +Code -- by :user:`aiolibsbot`. diff --git a/CHANGES/12618.contrib.rst b/CHANGES/12618.contrib.rst new file mode 120000 index 00000000000..7efbc0b7ed6 --- /dev/null +++ b/CHANGES/12618.contrib.rst @@ -0,0 +1 @@ +12617.contrib.rst \ No newline at end of file diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst new file mode 100644 index 00000000000..0c7e2a0a548 --- /dev/null +++ b/CHANGES/12624.contrib.rst @@ -0,0 +1,4 @@ +Added a default 120-second per-test timeout via ``pytest-timeout`` so a +hung test surfaces by name in CI output instead of getting hidden behind +the job-level timeout added in :pr:`12619`. The ``autobahn`` and +benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`. diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c2d36..30ceb187918 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,4 @@ +@~/.claude/aio-libs/context.md +@~/.claude/aio-libs/aiohttp/context.md @AGENTS.md +@CLAUDE.local.md diff --git a/requirements/test-common.in b/requirements/test-common.in index b50aed3be7f..9b3c5839b1f 100644 --- a/requirements/test-common.in +++ b/requirements/test-common.in @@ -9,6 +9,7 @@ pytest pytest-aiohttp pytest-cov pytest-mock +pytest-timeout pytest-xdist pytest_codspeed python-on-whales diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 94aa95d62be..1faa93e7c1f 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -111,6 +111,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index fcad3f25513..073a4360a15 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -134,6 +134,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/test.txt b/requirements/test.txt index 16cb56e094f..3c84d9304c1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -134,6 +134,8 @@ pytest-cov==7.1.0 # via -r requirements/test-common.in pytest-mock==3.15.1 # via -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/setup.cfg b/setup.cfg index 8a721800cf3..e78606ecd57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,9 @@ addopts = # Disable entry-point auto-load, otherwise we miss coverage. -p no:aiohttp asyncio_mode = auto +# 2-minute per-test timeout so a hung test surfaces by name instead of taking +# down the whole job. Autobahn and benchmark jobs override with `--timeout=0`. +timeout = 120 filterwarnings = error ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index cfc2d4529c5..10953516a0a 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -2416,7 +2416,7 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N we raise DecompressSizeError. """ # Create a highly compressible payload. - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressed = zlib.compress(original) assert len(original) > DEFAULT_CHUNK_SIZE @@ -2448,7 +2448,7 @@ async def test_payload_decompress_size_limit_brotli( """Test that brotli decompression size limit triggers DecompressSizeError.""" assert brotli is not None # Create a highly compressible payload - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressed = brotli.compress(original) assert len(original) > DEFAULT_CHUNK_SIZE @@ -2479,7 +2479,7 @@ async def test_payload_decompress_size_limit_zstd( """Test that zstd decompression size limit triggers DecompressSizeError.""" assert ZstdCompressor is not None # Create a highly compressible payload. - payload_size = 64 * 2**20 + payload_size = 2 * 2**20 original = b"A" * payload_size compressor = ZstdCompressor() compressed = compressor.compress(original) + compressor.flush() @@ -2904,7 +2904,7 @@ async def handler(request: web.Request) -> web.Response: assert 200 == resp.status cookie_names = {c.key for c in client.session.cookie_jar} assert cookie_names == {"c1", "c2", "c3"} - await asyncio.sleep(2) + await asyncio.sleep(1.1) cookie_names = {c.key for c in client.session.cookie_jar} assert cookie_names == {"c1", "c2"} diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index fa71d9aa2ed..147785def1c 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1155,9 +1155,10 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None: async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: + num_chunks = 20000 # comfortably above the 16385 pause threshold text = ( b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n" - + b"1\r\nb\r\n" * 50000 + + b"1\r\nb\r\n" * num_chunks + b"0\r\n\r\n" ) @@ -1168,8 +1169,9 @@ async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None: assert len(payload._http_chunk_splits) == 16385 # We should still get the full result after read(), as it will continue processing. result = await payload.read() - assert len(result) == 50000 # Compare len first, as it's easier to debug in diff. - assert result == b"b" * 50000 + # Compare len first, as it's easier to debug in diff. + assert len(result) == num_chunks + assert result == b"b" * num_chunks async def test_compressed_with_tail(response: HttpResponseParser) -> None: