f591e258f7
Database opened its sqlite3 connection eagerly in __init__ and closed it
in close(); the lifespan called close() on shutdown. In production this
is fine — the lifespan runs once per process. Under pytest the module-
level ``db`` singleton survives across every TestClient session, so the
second test file's lifespan startup hit
``sqlite3.ProgrammingError: Cannot operate on a closed database`` at
fixture-setup time (AutoBackupEngine.__init__ → db.get_setting("…")
was the first reader). 65 spurious "errors" on a full Windows pytest run.
- Database: extract _open() from __init__, add ensure_open() that
reopens iff _conn is None, and have close() null _conn after the
TRUNCATE checkpoint so re-close is idempotent.
- main.py lifespan startup: call db.ensure_open() before any setting
read, so subsequent TestClient sessions get a live connection.
- tests/storage/test_database_reopen.py: pin the four invariants —
close→ensure_open round-trips data, ensure_open is a no-op when
open, close is idempotent, and using the DB after close without
ensure_open raises (callers must opt in).
Full backend suite: 1551 pass / 1 skip / 0 errors. Ruff clean.
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""Regression coverage for the lifespan reopen path on ``Database``.
|
|
|
|
The production server only runs a single FastAPI lifespan per process, so
|
|
``Database.close()`` releasing the connection is fine. Pytest is different:
|
|
``ledgrab.main`` is imported once and its module-level ``db`` singleton
|
|
survives across every TestClient session, each of which closes the
|
|
connection on shutdown.
|
|
|
|
Before :meth:`Database.ensure_open` existed, the second TestClient session
|
|
hit ``sqlite3.ProgrammingError: Cannot operate on a closed database`` at
|
|
fixture-setup time when the lifespan startup tried to read settings (the
|
|
``AutoBackupEngine`` constructor was the first reader). These tests pin
|
|
the close+reopen cycle so the cascade can't silently come back.
|
|
"""
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from ledgrab.storage.database import Database
|
|
|
|
|
|
def test_close_then_ensure_open_reopens_connection(tmp_path):
|
|
db = Database(tmp_path / "reopen.db")
|
|
db.set_setting("k", {"v": 1})
|
|
db.close()
|
|
|
|
db.ensure_open()
|
|
|
|
assert db.get_setting("k") == {"v": 1}
|
|
db.close()
|
|
|
|
|
|
def test_ensure_open_is_idempotent_when_already_open(tmp_path):
|
|
db = Database(tmp_path / "idempotent.db")
|
|
original_conn = db._conn
|
|
|
|
db.ensure_open()
|
|
|
|
# Same live connection — no spurious reconnect when already open.
|
|
assert db._conn is original_conn
|
|
db.close()
|
|
|
|
|
|
def test_close_is_idempotent(tmp_path):
|
|
db = Database(tmp_path / "double_close.db")
|
|
db.close()
|
|
|
|
# Second close must not raise (lifespan shutdown can run twice in
|
|
# quick test sessions). Connection stays released.
|
|
db.close()
|
|
assert db._conn is None
|
|
|
|
|
|
def test_operation_after_close_without_reopen_raises(tmp_path):
|
|
db = Database(tmp_path / "no_reopen.db")
|
|
db.close()
|
|
|
|
# Without ensure_open, attempting to use the DB fails loudly rather
|
|
# than silently re-connecting — callers must opt in.
|
|
with pytest.raises((sqlite3.ProgrammingError, AttributeError)):
|
|
db.get_setting("anything")
|