Fix hanging test suite by closing per-loop Tortoise contexts
pwo.async_test runs every test in a fresh event loop and TortoiseMixin builds one TortoiseContext per loop. Dropping the previous context when the loop changed orphaned its aiosqlite connections; their non-daemon worker threads then blocked threading._shutdown forever, so the suite printed OK but the interpreter never exited. Add TortoiseMixin.aclose() and a tests.helpers.async_test wrapper that closes the context in a finally on the test's own loop, and switch the test modules over to it.
This commit is contained in:
@@ -83,6 +83,22 @@ class TortoiseMixin(KayaMixin):
|
||||
self._ctx = None
|
||||
self._init_loop = None
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Close the current context's connections and forget it.
|
||||
|
||||
Must be called from the event loop that owns the context. When a
|
||||
loop goes away without this, its aiosqlite connections are orphaned;
|
||||
their non-daemon worker threads then block interpreter shutdown
|
||||
forever. The test suite calls this at the end of every test because
|
||||
each test runs in a fresh event loop.
|
||||
"""
|
||||
ctx = self._ctx
|
||||
self._ctx = None
|
||||
self._init_loop = None
|
||||
if ctx is not None:
|
||||
log.info("closing database connections")
|
||||
await ctx.close_connections()
|
||||
|
||||
async def _build_context(self) -> TortoiseContext:
|
||||
ctx = TortoiseContext()
|
||||
with ctx:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test helpers package."""
|
||||
from .asynctest import async_test
|
||||
from .oidc import make_user, oidc_user, ws_users
|
||||
|
||||
__all__ = ["make_user", "oidc_user", "ws_users"]
|
||||
__all__ = ["async_test", "make_user", "oidc_user", "ws_users"]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""An ``async_test`` that also closes the app's Tortoise context.
|
||||
|
||||
``pwo.async_test`` runs every test in a fresh event loop (``asyncio.Runner``).
|
||||
The app's :class:`~tavolo.tortoise_mixin.TortoiseMixin` builds one
|
||||
``TortoiseContext`` per loop, so without an explicit close each test orphans
|
||||
an aiosqlite connection whose non-daemon worker thread keeps the interpreter
|
||||
alive after the suite reports "OK".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Coroutine
|
||||
|
||||
from tavolo.app import tortoise_mixin
|
||||
|
||||
|
||||
def async_test(coro: Callable[..., Coroutine[Any, Any, None]]) -> Callable[..., None]:
|
||||
"""Like ``pwo.async_test``, but close the Tortoise context afterwards."""
|
||||
|
||||
@wraps(coro)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||
async def run() -> None:
|
||||
try:
|
||||
await coro(*args, **kwargs)
|
||||
finally:
|
||||
await tortoise_mixin.aclose()
|
||||
|
||||
with asyncio.Runner() as runner:
|
||||
runner.run(run())
|
||||
|
||||
return wrapper
|
||||
@@ -14,10 +14,10 @@ from unittest.mock import patch
|
||||
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from kaya.core import HttpContext, KayaApp
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.app import cors_mixin_from_settings
|
||||
from tavolo.config import Settings
|
||||
from tests.helpers import async_test
|
||||
|
||||
ORIGIN = "https://cards.example"
|
||||
|
||||
|
||||
@@ -11,12 +11,11 @@ import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo import deadlines
|
||||
from tavolo.app import game_store
|
||||
from tavolo.game import engine
|
||||
from tavolo.game.state import GameState, PlayerState
|
||||
from tests.helpers import async_test
|
||||
|
||||
PLAYERS = ("alice", "bob", "carol", "dave")
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.app import app
|
||||
from tests.helpers import oidc_user
|
||||
from tests.helpers import async_test, oidc_user
|
||||
|
||||
|
||||
class GamesRouteTest(unittest.TestCase):
|
||||
|
||||
@@ -8,11 +8,10 @@ from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.app import app
|
||||
from tavolo.config import settings
|
||||
from tests.helpers import oidc_user
|
||||
from tests.helpers import async_test, oidc_user
|
||||
|
||||
|
||||
class MeRouteTest(unittest.TestCase):
|
||||
|
||||
@@ -6,7 +6,6 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.app import app, tortoise_mixin
|
||||
from tavolo.elo import INITIAL_RATING
|
||||
@@ -14,7 +13,7 @@ from tavolo.game import engine
|
||||
from tavolo.game.state import GameState
|
||||
from tavolo.models import Match, MatchPlayer, PlayerRating
|
||||
from tavolo.stats import save_match_result
|
||||
from tests.helpers import oidc_user
|
||||
from tests.helpers import async_test, oidc_user
|
||||
|
||||
|
||||
async def _use_app_db():
|
||||
|
||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import unittest
|
||||
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.game import engine
|
||||
from tavolo.store import InMemoryGameStore
|
||||
from tests.helpers import async_test
|
||||
|
||||
|
||||
class InMemoryGameStoreTest(unittest.TestCase):
|
||||
|
||||
@@ -7,12 +7,11 @@ import unittest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from httpx_ws import WebSocketDisconnect, aconnect_ws
|
||||
from httpx_ws.transport import ASGIWebSocketTransport
|
||||
from pwo import async_test
|
||||
|
||||
from tavolo.app import app, game_store
|
||||
from tavolo.game import engine
|
||||
from tavolo.game.state import Card, GameState, PlayerState
|
||||
from tests.helpers import make_user, oidc_user, ws_users
|
||||
from tests.helpers import async_test, make_user, oidc_user, ws_users
|
||||
|
||||
PLAYERS = ("alice", "bob", "carol", "dave")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user