Explorar el Código

test: Docker test runs use all logical CPUs (-n logical; auto counted physical cores only); tests stop the background tasks they started and close the app engine's connections after every test on SQLite too ("Event loop is closed" thread warning)

maziggy hace 1 día
padre
commit
198610476f
Se han modificado 3 ficheros con 40 adiciones y 17 borrados
  1. 4 3
      Dockerfile.test
  2. 34 12
      backend/tests/conftest.py
  3. 2 2
      docker-compose.test.yml

+ 4 - 3
Dockerfile.test

@@ -46,10 +46,11 @@ HEALTHCHECK NONE
 # -v dropped: 5300+ "PASSED foo::bar" lines per worker eat noticeable
 # stdout I/O time and clutter test_docker.sh output. --tb=short still
 # gives full failure tracebacks when something breaks.
-# -n auto adapts to the host's vCPU count instead of hard-coding 30 —
+# -n logical adapts to the host's vCPU count instead of hard-coding 30 —
 # on a 2-vCPU CI / VM runner, -n 30 spawns 30 Python processes fighting
-# for 2 cores, which is mostly IPC + import-thrash overhead.
-CMD ["pytest", "backend/tests/", "--tb=short", "-p", "no:cacheprovider", "-n", "auto"]
+# for 2 cores, which is mostly IPC + import-thrash overhead. Not -n auto:
+# with psutil installed that counts physical cores only (16 of 30 here).
+CMD ["pytest", "backend/tests/", "--tb=short", "-p", "no:cacheprovider", "-n", "logical"]
 
 # -------------------------------------------
 # Frontend test stage

+ 34 - 12
backend/tests/conftest.py

@@ -417,6 +417,24 @@ async def _finish_query_cancellations() -> None:
         await asyncio.wait(pending, timeout=5)
 
 
+async def _stop_background_tasks() -> None:
+    """Cancel the background tasks this test's code spawned, and wait for them.
+
+    A task still in a query when the loop closes gets its answer from
+    aiosqlite's worker thread into the closed loop ("Event loop is closed"),
+    reported as a thread exception in whatever test runs next on the worker.
+    Cancelled here, its session closes while the loop still runs.
+    """
+    from backend.app.core.tasks import _background_tasks
+
+    loop = asyncio.get_running_loop()
+    pending = [task for task in _background_tasks if not task.done() and task.get_loop() is loop]
+    for task in pending:
+        task.cancel()
+    if pending:
+        await asyncio.wait(pending, timeout=5)
+
+
 async def _release_app_engine() -> None:
     """Close the app engine's pooled connections before this test's loop ends.
 
@@ -427,18 +445,24 @@ async def _release_app_engine() -> None:
     PostgreSQL that leaves asyncpg's cleanup unawaited, reported as an
     unraisable "Connection._cancel" coroutine in some later test.
     """
+    # First: a task still holding a connection would return it to the pool
+    # after the dispose below has replaced that pool, and it is never closed.
+    await _stop_background_tasks()
     await _finish_query_cancellations()
     await _app_engine.dispose()
 
 
-if TEST_PG_URL is not None:
+@pytest.fixture(autouse=True)
+async def _app_engine_released_after_each_test():
+    """Every test, not only those with test_engine: any of them can reach
+    the app engine through code that opens its own session.
 
-    @pytest.fixture(autouse=True)
-    async def _app_engine_released_after_each_test():
-        """Every test, not only those with test_engine: any of them can reach
-        the app engine through code that opens its own session."""
-        yield
-        await _release_app_engine()
+    On SQLite too: an aiosqlite connection belongs to the loop it was opened
+    in, so one kept in the pool answers the next test's query into a closed
+    loop ("Event loop is closed" from aiosqlite's worker thread).
+    """
+    yield
+    await _release_app_engine()
 
 
 @pytest.fixture
@@ -536,11 +560,9 @@ async def async_client(test_engine, db_session) -> AsyncGenerator[AsyncClient, N
             yield client
 
         # The app lifespan called init_db() which used the module-level engine
-        # (not the test engine), creating aiosqlite connections. Dispose those
-        # connections so their background threads finish before the event loop closes.
-        from backend.app.core.database import engine as real_engine
-
-        await real_engine.dispose()
+        # (not the test engine), creating aiosqlite connections. Close those,
+        # and stop background work still using them, before the loop closes.
+        await _release_app_engine()
 
     app.dependency_overrides.clear()
 

+ 2 - 2
docker-compose.test.yml

@@ -62,9 +62,9 @@ services:
     environment:
       - BAMBUDDY_TEST_URL=http://integration:8000
       - TESTING=1
-    # -v dropped + -n auto so integration tests inherit the same noise /
+    # -v dropped + -n logical so integration tests inherit the same noise /
     # parallelism profile as the backend-test image (see Dockerfile.test CMD).
-    command: ["pytest", "backend/tests/integration/", "--tb=short", "-p", "no:cacheprovider", "-n", "auto"]
+    command: ["pytest", "backend/tests/integration/", "--tb=short", "-p", "no:cacheprovider", "-n", "logical"]
     init: true
     volumes:
       - ./backend:/app/backend:ro