Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2b514ab91 | ||
|
|
031d933204 |
@@ -83,6 +83,22 @@ class TortoiseMixin(KayaMixin):
|
|||||||
self._ctx = None
|
self._ctx = None
|
||||||
self._init_loop = 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:
|
async def _build_context(self) -> TortoiseContext:
|
||||||
ctx = TortoiseContext()
|
ctx = TortoiseContext()
|
||||||
with ctx:
|
with ctx:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Test helpers package."""
|
"""Test helpers package."""
|
||||||
|
from .asynctest import async_test
|
||||||
from .oidc import make_user, oidc_user, ws_users
|
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 httpx import ASGITransport, AsyncClient
|
||||||
from kaya.core import HttpContext, KayaApp
|
from kaya.core import HttpContext, KayaApp
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.app import cors_mixin_from_settings
|
from tavolo.app import cors_mixin_from_settings
|
||||||
from tavolo.config import Settings
|
from tavolo.config import Settings
|
||||||
|
from tests.helpers import async_test
|
||||||
|
|
||||||
ORIGIN = "https://cards.example"
|
ORIGIN = "https://cards.example"
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ import unittest
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo import deadlines
|
from tavolo import deadlines
|
||||||
from tavolo.app import game_store
|
from tavolo.app import game_store
|
||||||
from tavolo.game import engine
|
from tavolo.game import engine
|
||||||
from tavolo.game.state import GameState, PlayerState
|
from tavolo.game.state import GameState, PlayerState
|
||||||
|
from tests.helpers import async_test
|
||||||
|
|
||||||
PLAYERS = ("alice", "bob", "carol", "dave")
|
PLAYERS = ("alice", "bob", "carol", "dave")
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.app import app
|
from tavolo.app import app
|
||||||
from tests.helpers import oidc_user
|
from tests.helpers import async_test, oidc_user
|
||||||
|
|
||||||
|
|
||||||
class GamesRouteTest(unittest.TestCase):
|
class GamesRouteTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.app import app
|
from tavolo.app import app
|
||||||
from tavolo.config import settings
|
from tavolo.config import settings
|
||||||
from tests.helpers import oidc_user
|
from tests.helpers import async_test, oidc_user
|
||||||
|
|
||||||
|
|
||||||
class MeRouteTest(unittest.TestCase):
|
class MeRouteTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import uuid
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.app import app, tortoise_mixin
|
from tavolo.app import app, tortoise_mixin
|
||||||
from tavolo.elo import INITIAL_RATING
|
from tavolo.elo import INITIAL_RATING
|
||||||
@@ -14,7 +13,7 @@ from tavolo.game import engine
|
|||||||
from tavolo.game.state import GameState
|
from tavolo.game.state import GameState
|
||||||
from tavolo.models import Match, MatchPlayer, PlayerRating
|
from tavolo.models import Match, MatchPlayer, PlayerRating
|
||||||
from tavolo.stats import save_match_result
|
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():
|
async def _use_app_db():
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.game import engine
|
from tavolo.game import engine
|
||||||
from tavolo.store import InMemoryGameStore
|
from tavolo.store import InMemoryGameStore
|
||||||
|
from tests.helpers import async_test
|
||||||
|
|
||||||
|
|
||||||
class InMemoryGameStoreTest(unittest.TestCase):
|
class InMemoryGameStoreTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import unittest
|
|||||||
from httpx import ASGITransport, AsyncClient
|
from httpx import ASGITransport, AsyncClient
|
||||||
from httpx_ws import WebSocketDisconnect, aconnect_ws
|
from httpx_ws import WebSocketDisconnect, aconnect_ws
|
||||||
from httpx_ws.transport import ASGIWebSocketTransport
|
from httpx_ws.transport import ASGIWebSocketTransport
|
||||||
from pwo import async_test
|
|
||||||
|
|
||||||
from tavolo.app import app, game_store
|
from tavolo.app import app, game_store
|
||||||
from tavolo.game import engine
|
from tavolo.game import engine
|
||||||
from tavolo.game.state import Card, GameState, PlayerState
|
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")
|
PLAYERS = ("alice", "bob", "carol", "dave")
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ pub fn summary_rows(summary: HandSummary) -> View {
|
|||||||
// Denara
|
// Denara
|
||||||
award_row(
|
award_row(
|
||||||
card_img("02D".to_string(), "score-mini"),
|
card_img("02D".to_string(), "score-mini"),
|
||||||
"Denara",
|
"Denari",
|
||||||
match &summary.award.denara {
|
match &summary.award.denara {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
let (w, l) = winner_first(summary.denara.a, summary.denara.b, Some(t));
|
let (w, l) = winner_first(summary.denara.a, summary.denara.b, Some(t));
|
||||||
@@ -271,7 +271,7 @@ pub fn hand_summary_modal(
|
|||||||
if let Some(s) = socket.get_clone() {
|
if let Some(s) = socket.get_clone() {
|
||||||
s.ack();
|
s.ack();
|
||||||
}
|
}
|
||||||
}) { "Understood — next hand" }
|
}) { "Understood, next hand" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user