Skip to content

fix(tests): let pytest-asyncio own the suite, drop anyio markers - #6

Merged
ClaraVnk merged 1 commit into
mainfrom
fix/single-async-plugin
Jul 20, 2026
Merged

fix(tests): let pytest-asyncio own the suite, drop anyio markers#6
ClaraVnk merged 1 commit into
mainfrom
fix/single-async-plugin

Conversation

@ClaraVnk

Copy link
Copy Markdown
Contributor

Contexte

Test (redis) has been red on main since the toolchain drifted (baseline 29729210049), failing on a fixture teardown rather than a test:

ERROR at teardown of <last test of tests/test_api.py>
RuntimeError: Event loop is closed
  tests/test_api.py:26: await app.state.storage.close()

Every test passed; only the finalizer errored. Test (sqlite) was always green.

Root cause

Two async plugins were driving the same tests. The suite carried @pytest.mark.anyio markers while pyproject.toml sets asyncio_mode = "auto". anyio parametrized the marked tests — hence the [asyncio] suffix in the test IDs — and ran them in loops it managed itself, while the module-scoped client fixture stayed on pytest-asyncio's module loop.

The Redis connection pool therefore accumulated connections opened on per-test loops that were already closed by the time the module fixture tore down. disconnect() then touched dead transports. SQLite never surfaced it because aiosqlite is loop-agnostic.

Note the failing teardown ran on a live loop — the loop the pool's connections belonged to was a different, dead one. That is why "is the loop closed at teardown?" kept giving a misleading answer.

Evidence

The failure never reproduced locally, so I instrumented CI directly (29730652856 before, 29730869139 after):

fixture setup teardown finalization
before no current event loop live loop, still errors no current event loop
after id=140216260195536 same id same id

One coherent loop per module, and all three jobs green.

Changements

  • Remove the 47 @pytest.mark.anyio markers across 4 test files. asyncio_mode = "auto" already handles every async test, so they were redundant — and they were the thing introducing a second loop owner.
  • Document the invariant in pyproject.toml, next to the existing module-scope rationale, so it does not regress.

No production code touched. Test count is unchanged at 114, and the [asyncio] parametrization is gone from the IDs.

Tests

  • CI, on the instrumented branch carrying this exact change: Lint, Test (sqlite) and Test (redis) all green, 114 passed.
  • Local, CPython 3.14.4 and 3.14.6 (via python:3.14.6-slim), both backends against redis:7-alpine: 114 passed.
  • ruff check / ruff format --check clean.

Risques

The real risk is a marker removal silently turning async tests into no-ops. Guarded against: collection still reports exactly 114 tests, and the Redis-backed run genuinely exercises the backend (it is what surfaced this bug in the first place).

Earlier attempt #4 was closed after its diagnosis was falsified; this supersedes it.

Test (redis) has been failing on main since the toolchain drifted, with
"RuntimeError: Event loop is closed" raised from the module-scoped `client`
fixture teardown. Every test passed; only the finalizer errored.

Two async plugins were driving the same tests. The suite carried
@pytest.mark.anyio while pyproject sets asyncio_mode = "auto", so anyio
parametrized the marked tests ("[asyncio]") and ran them in loops it managed
itself, while the module-scoped fixtures stayed on pytest-asyncio's loop. The
Redis connection pool therefore accumulated connections opened on per-test
loops that were closed by the time the module fixture tore down, and
disconnecting them touched dead transports. SQLite never showed it because
aiosqlite is loop-agnostic.

CI instrumentation confirmed both the cause and the fix: before, the fixture
setup reported no current event loop; after, setup, teardown and finalization
all report the same loop.

asyncio_mode = "auto" already handles every async test, so the 47 markers were
redundant. Removing them leaves a single owner for the loop lifecycle. The
invariant is documented in pyproject.toml so it does not regress.
@ClaraVnk
ClaraVnk merged commit 316542c into main Jul 20, 2026
3 checks passed
@ClaraVnk
ClaraVnk deleted the fix/single-async-plugin branch July 20, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant