Coverage for tests/test_structured_logging.py: 98%
121 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
1"""Structured Query Runtime logging tests."""
3from __future__ import annotations
5from pathlib import Path
6from sqlite3 import connect
7from tempfile import TemporaryDirectory
8from typing import Any, cast
10from snektest import assert_eq, assert_raises, assert_true, test
12from snekql import (
13 MISSING,
14 Database,
15 ExecutionError,
16 Fetched,
17 Integer,
18 Model,
19 Pending,
20 PoolTimeoutError,
21 Text,
22 insert,
23 select,
24)
27class _RecordingStructuredLogger:
28 """Structured logger fake that records event calls for assertions."""
30 def __init__(self) -> None:
31 self.events: list[tuple[str, str, dict[str, object]]] = []
33 def debug(self, event: str, **fields: object) -> None:
34 self.events.append(("debug", event, fields))
36 def info(self, event: str, **fields: object) -> None:
37 self.events.append(("info", event, fields))
39 def warning(self, event: str, **fields: object) -> None:
40 self.events.append(("warning", event, fields))
42 def error(self, event: str, **fields: object) -> None:
43 self.events.append(("error", event, fields))
45 def find(self, event: str) -> dict[str, object]:
46 """Return the first recorded event fields for the named event."""
48 for _level, recorded_event, fields in self.events:
49 if recorded_event == event:
50 return fields
51 msg = f"event not recorded: {event}"
52 raise AssertionError(msg)
54 def has(self, level: str, event: str) -> bool:
55 """Return whether a level/event pair was recorded."""
57 return any(
58 recorded_level == level and recorded_event == event
59 for recorded_level, recorded_event, _fields in self.events
60 )
63def _execute_sql(database_path: Path, sql: str) -> None:
64 connection = connect(database_path)
65 try:
66 _ = connection.execute(sql)
67 connection.commit()
68 finally:
69 connection.close()
72@test()
73def database_initialization_requires_a_logger() -> None:
74 """The public initialization path requires explicit structured logging."""
76 initialize = cast("Any", Database.initialize)
78 with assert_raises(TypeError):
79 _ = initialize(database=":memory:")
81 with assert_raises(TypeError):
82 _ = initialize(_RecordingStructuredLogger(), database=":memory:")
85@test(mark="medium")
86async def database_initialization_emits_structured_events() -> None:
87 """Database initialization logs backend and schema startup context."""
89 class User[S = Pending](Model[S, "User[Fetched]"]):
90 """Table model used to observe schema startup logging."""
92 id: User.GenCol[int] = Integer(
93 primary_key=True,
94 auto_increment=True,
95 default=MISSING,
96 )
97 email: User.Col[str] = Text(nullable=False)
99 logger = _RecordingStructuredLogger()
100 with TemporaryDirectory() as directory:
101 database_path = Path(directory) / "app.db"
102 database = await Database.initialize(
103 logger=logger,
104 database=database_path,
105 models=[User],
106 )
107 await database.close()
109 started = logger.find("database initialization started")
110 completed = logger.find("database initialization completed")
111 created = logger.find("schema table created")
113 assert_eq(started["backend"], "sqlite")
114 assert_eq(started["model_count"], 1)
115 assert_eq(started["table_names"], ("user",))
116 assert_eq(completed["backend"], "sqlite")
117 assert_eq(created["table_name"], "user")
118 assert_true(logger.has("info", "database initialization started"))
119 assert_true(logger.has("info", "database initialization completed"))
122@test(mark="medium")
123async def warn_schema_policy_uses_injected_structured_logger() -> None:
124 """Warn schema verification reports drift through the supplied logger."""
126 class User[S = Pending](Model[S, "User[Fetched]"]):
127 """Table model used for warn policy drift logging."""
129 email: User.Col[str] = Text(nullable=False)
131 logger = _RecordingStructuredLogger()
132 with TemporaryDirectory() as directory:
133 database_path = Path(directory) / "app.db"
134 _execute_sql(database_path, 'CREATE TABLE "user" ("email" TEXT NOT NULL)')
136 database = await Database.initialize(
137 logger=logger,
138 database=database_path,
139 models=[User],
140 schema_policy="warn",
141 )
142 await database.close()
144 drift = logger.find("schema drift detected")
145 assert_eq(drift["table_name"], "user")
146 assert_true(logger.has("warning", "schema drift detected"))
149@test(mark="medium")
150async def transaction_execution_emits_query_context() -> None:
151 """Transaction logging includes SQL and params without redaction."""
153 class User[S = Pending](Model[S, "User[Fetched]"]):
154 """Table model used to observe query execution logging."""
156 id: User.GenCol[int] = Integer(
157 primary_key=True,
158 auto_increment=True,
159 default=MISSING,
160 )
161 email: User.Col[str] = Text(nullable=False)
163 logger = _RecordingStructuredLogger()
164 database = await Database.initialize(
165 logger=logger,
166 database=":memory:",
167 models=[User],
168 )
169 try:
170 async with database.transaction() as tx:
171 await tx.execute(insert(User(email="secret@example.com")))
172 row = await tx.fetch_one(
173 select(User.email).where(User.email.eq("secret@example.com"))
174 )
175 finally:
176 await database.close()
178 assert_eq(row, "secret@example.com")
179 write = next(
180 fields
181 for _level, event, fields in logger.events
182 if event == "query executed" and fields["operation"] == "write"
183 )
184 select_event = next(
185 fields
186 for _level, event, fields in logger.events
187 if event == "query executed" and fields["operation"] == "fetch_one"
188 )
190 assert_eq(write["params"], ("secret@example.com",))
191 assert_eq(select_event["params"], ("secret@example.com",))
192 assert_true(logger.has("debug", "transaction begin"))
193 assert_true(logger.has("debug", "transaction commit"))
196@test(mark="medium")
197async def query_failure_emits_structured_error_context() -> None:
198 """Execution failures log SQL and params before raising ExecutionError."""
200 class User[S = Pending](Model[S, "User[Fetched]"]):
201 """Table model with a unique field used to force a write failure."""
203 email: User.Col[str] = Text(nullable=False, unique=True)
205 logger = _RecordingStructuredLogger()
206 database = await Database.initialize(
207 logger=logger,
208 database=":memory:",
209 models=[User],
210 )
211 try:
212 async with database.transaction() as tx:
213 await tx.execute(insert(User(email="duplicate@example.com")))
214 with assert_raises(ExecutionError):
215 await tx.execute(insert(User(email="duplicate@example.com")))
216 finally:
217 await database.close()
219 failure = logger.find("query failed")
220 assert_eq(failure["operation"], "write")
221 assert_eq(failure["params"], ("duplicate@example.com",))
222 assert_true(logger.has("error", "query failed"))
225@test(mark="medium")
226async def pool_timeout_emits_structured_warning() -> None:
227 """Pool acquisition timeouts are logged while preserving the public error."""
229 logger = _RecordingStructuredLogger()
230 database = await Database.initialize(
231 logger=logger,
232 database=":memory:",
233 acquire_timeout=0.0,
234 pool_size=1,
235 )
236 try:
237 async with database.transaction():
238 with assert_raises(PoolTimeoutError):
239 async with database.transaction(timeout=0.0):
240 pass
241 finally:
242 await database.close()
244 timeout = logger.find("connection acquisition timed out")
245 assert_eq(timeout["backend"], "sqlite")
246 assert_eq(timeout["timeout"], 0.0)
247 assert_true(logger.has("warning", "connection acquisition timed out"))