Skip to content

Commit 802f89b

Browse files
test: add tests for fetchone, fetchall, fetchval, and protocol methods
Fills major coverage gaps: - fetchone (returns first row, returns None for empty) - fetchall (returns list of lists) - fetchval (returns first column, returns None for empty) - protocol get_leader, prepare, finalize - protocol connection-closed-during-read error path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ae89bfc commit 802f89b

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

tests/test_connection.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,158 @@ async def test_connection_invalidated_after_protocol_error(self) -> None:
231231

232232
# Connection should be invalidated
233233
assert not conn.is_connected
234+
235+
async def test_fetchone_returns_first_row(self) -> None:
236+
conn = DqliteConnection("localhost:9001")
237+
238+
mock_reader = AsyncMock()
239+
mock_writer = MagicMock()
240+
mock_writer.drain = AsyncMock()
241+
mock_writer.close = MagicMock()
242+
mock_writer.wait_closed = AsyncMock()
243+
244+
from dqlitewire.constants import ValueType
245+
from dqlitewire.messages import DbResponse, RowsResponse, WelcomeResponse
246+
247+
responses = [
248+
WelcomeResponse(heartbeat_timeout=15000).encode(),
249+
DbResponse(db_id=1).encode(),
250+
RowsResponse(
251+
column_names=["id", "name"],
252+
column_types=[ValueType.INTEGER, ValueType.TEXT],
253+
rows=[[1, "first"], [2, "second"]],
254+
has_more=False,
255+
).encode(),
256+
]
257+
mock_reader.read.side_effect = responses
258+
259+
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
260+
await conn.connect()
261+
262+
mock_reader.read.side_effect = [responses[2]]
263+
result = await conn.fetchone("SELECT * FROM t")
264+
assert result == {"id": 1, "name": "first"}
265+
266+
async def test_fetchone_returns_none_for_empty(self) -> None:
267+
conn = DqliteConnection("localhost:9001")
268+
269+
mock_reader = AsyncMock()
270+
mock_writer = MagicMock()
271+
mock_writer.drain = AsyncMock()
272+
mock_writer.close = MagicMock()
273+
mock_writer.wait_closed = AsyncMock()
274+
275+
from dqlitewire.constants import ValueType
276+
from dqlitewire.messages import DbResponse, RowsResponse, WelcomeResponse
277+
278+
responses = [
279+
WelcomeResponse(heartbeat_timeout=15000).encode(),
280+
DbResponse(db_id=1).encode(),
281+
]
282+
mock_reader.read.side_effect = responses
283+
284+
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
285+
await conn.connect()
286+
287+
empty_response = RowsResponse(
288+
column_names=["id"],
289+
column_types=[ValueType.INTEGER],
290+
rows=[],
291+
has_more=False,
292+
).encode()
293+
mock_reader.read.side_effect = [empty_response]
294+
result = await conn.fetchone("SELECT * FROM t WHERE 1=0")
295+
assert result is None
296+
297+
async def test_fetchall_returns_lists(self) -> None:
298+
conn = DqliteConnection("localhost:9001")
299+
300+
mock_reader = AsyncMock()
301+
mock_writer = MagicMock()
302+
mock_writer.drain = AsyncMock()
303+
mock_writer.close = MagicMock()
304+
mock_writer.wait_closed = AsyncMock()
305+
306+
from dqlitewire.constants import ValueType
307+
from dqlitewire.messages import DbResponse, RowsResponse, WelcomeResponse
308+
309+
responses = [
310+
WelcomeResponse(heartbeat_timeout=15000).encode(),
311+
DbResponse(db_id=1).encode(),
312+
]
313+
mock_reader.read.side_effect = responses
314+
315+
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
316+
await conn.connect()
317+
318+
rows_response = RowsResponse(
319+
column_names=["id", "name"],
320+
column_types=[ValueType.INTEGER, ValueType.TEXT],
321+
rows=[[1, "a"], [2, "b"]],
322+
has_more=False,
323+
).encode()
324+
mock_reader.read.side_effect = [rows_response]
325+
result = await conn.fetchall("SELECT * FROM t")
326+
assert result == [[1, "a"], [2, "b"]]
327+
328+
async def test_fetchval_returns_first_column(self) -> None:
329+
conn = DqliteConnection("localhost:9001")
330+
331+
mock_reader = AsyncMock()
332+
mock_writer = MagicMock()
333+
mock_writer.drain = AsyncMock()
334+
mock_writer.close = MagicMock()
335+
mock_writer.wait_closed = AsyncMock()
336+
337+
from dqlitewire.constants import ValueType
338+
from dqlitewire.messages import DbResponse, RowsResponse, WelcomeResponse
339+
340+
responses = [
341+
WelcomeResponse(heartbeat_timeout=15000).encode(),
342+
DbResponse(db_id=1).encode(),
343+
]
344+
mock_reader.read.side_effect = responses
345+
346+
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
347+
await conn.connect()
348+
349+
rows_response = RowsResponse(
350+
column_names=["count"],
351+
column_types=[ValueType.INTEGER],
352+
rows=[[42]],
353+
has_more=False,
354+
).encode()
355+
mock_reader.read.side_effect = [rows_response]
356+
result = await conn.fetchval("SELECT count(*) FROM t")
357+
assert result == 42
358+
359+
async def test_fetchval_returns_none_for_empty(self) -> None:
360+
conn = DqliteConnection("localhost:9001")
361+
362+
mock_reader = AsyncMock()
363+
mock_writer = MagicMock()
364+
mock_writer.drain = AsyncMock()
365+
mock_writer.close = MagicMock()
366+
mock_writer.wait_closed = AsyncMock()
367+
368+
from dqlitewire.constants import ValueType
369+
from dqlitewire.messages import DbResponse, RowsResponse, WelcomeResponse
370+
371+
responses = [
372+
WelcomeResponse(heartbeat_timeout=15000).encode(),
373+
DbResponse(db_id=1).encode(),
374+
]
375+
mock_reader.read.side_effect = responses
376+
377+
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
378+
await conn.connect()
379+
380+
empty_response = RowsResponse(
381+
column_names=["id"],
382+
column_types=[ValueType.INTEGER],
383+
rows=[],
384+
has_more=False,
385+
).encode()
386+
mock_reader.read.side_effect = [empty_response]
387+
result = await conn.fetchval("SELECT id FROM t WHERE 1=0")
388+
assert result is None

tests/test_protocol.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,59 @@ async def test_query_sql_multipart(
138138
assert rows[0] == [1, "alice"]
139139
assert rows[1] == [2, "bob"]
140140

141+
async def test_get_leader(
142+
self,
143+
protocol: DqliteProtocol,
144+
mock_reader: AsyncMock,
145+
leader_response: bytes,
146+
) -> None:
147+
mock_reader.read.return_value = leader_response
148+
149+
node_id, address = await protocol.get_leader()
150+
151+
assert node_id == 1
152+
assert address == "localhost:9001"
153+
154+
async def test_prepare(
155+
self,
156+
protocol: DqliteProtocol,
157+
mock_reader: AsyncMock,
158+
) -> None:
159+
from dqlitewire.messages import StmtResponse
160+
161+
mock_reader.read.return_value = StmtResponse(
162+
db_id=1, stmt_id=1, num_params=2
163+
).encode()
164+
165+
stmt_id, num_params = await protocol.prepare(1, "INSERT INTO t VALUES (?, ?)")
166+
167+
assert stmt_id == 1
168+
assert num_params == 2
169+
170+
async def test_finalize(
171+
self,
172+
protocol: DqliteProtocol,
173+
mock_reader: AsyncMock,
174+
) -> None:
175+
from dqlitewire.messages import EmptyResponse
176+
177+
mock_reader.read.return_value = EmptyResponse().encode()
178+
179+
# Should not raise
180+
await protocol.finalize(1, 1)
181+
182+
async def test_connection_closed_during_read(
183+
self,
184+
protocol: DqliteProtocol,
185+
mock_reader: AsyncMock,
186+
) -> None:
187+
mock_reader.read.return_value = b""
188+
189+
from dqliteclient.exceptions import DqliteConnectionError
190+
191+
with pytest.raises(DqliteConnectionError, match="Connection closed"):
192+
await protocol.exec_sql(1, "SELECT 1")
193+
141194
async def test_read_timeout(
142195
self,
143196
mock_reader: AsyncMock,

0 commit comments

Comments
 (0)