Files
Video-Tree-TRM5/tests/unit/test_telemetry.py
T

196 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""adapters/telemetry.py 单元测试 — SQLiteTelemetryRecorder。"""
from __future__ import annotations
import sqlite3
import uuid
import pytest
from adapters.telemetry import SQLiteTelemetryRecorder
from core.protocols import TelemetryRecorder
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def db_path(tmp_path):
"""返回临时数据库路径。"""
return tmp_path / "telemetry_test.db"
@pytest.fixture()
def recorder(db_path):
"""构造 SQLiteTelemetryRecorder 实例。"""
return SQLiteTelemetryRecorder(db_path=db_path)
def _make_call_kwargs(*, cache_hit: bool = False, error: str | None = None):
"""构造 record_llm_call 的标准参数字典。"""
return dict(
call_id=str(uuid.uuid4()),
parent_call_id=None,
session_id="sess-001",
model_name="gpt-4o",
provider="openai",
messages='[{"role":"user","content":"hi"}]',
response="hello",
thinking="",
prompt_tokens=10,
completion_tokens=5,
latency_ms=120,
ttft_ms=45.2,
max_inter_token_ms=12.3,
cache_hit=cache_hit,
error=error,
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_satisfies_protocol(recorder):
"""SQLiteTelemetryRecorder 满足 TelemetryRecorder Protocol。"""
assert isinstance(recorder, TelemetryRecorder)
@pytest.mark.asyncio
async def test_record_creates_table_and_inserts(recorder, db_path):
"""首次写入应懒创建表并成功插入一条记录。"""
kwargs = _make_call_kwargs()
await recorder.record_llm_call(**kwargs)
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT * FROM llm_calls").fetchall()
conn.close()
assert len(rows) == 1
row = rows[0]
assert row["call_id"] == kwargs["call_id"]
assert row["model_name"] == "gpt-4o"
assert row["prompt_tokens"] == 10
assert row["completion_tokens"] == 5
assert row["cache_hit"] == 0 # False → INTEGER 0
assert row["error"] is None
assert row["created_at"] is not None
@pytest.mark.asyncio
async def test_record_with_error(recorder, db_path):
"""error 字段非 None 时应正确存储。"""
kwargs = _make_call_kwargs(error="RateLimitError: 429")
await recorder.record_llm_call(**kwargs)
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
row = conn.execute(
"SELECT error FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)
).fetchone()
conn.close()
assert row["error"] == "RateLimitError: 429"
@pytest.mark.asyncio
async def test_record_cache_hit(recorder, db_path):
"""cache_hit=True 时应存储为 INTEGER 1。"""
kwargs = _make_call_kwargs(cache_hit=True)
await recorder.record_llm_call(**kwargs)
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
row = conn.execute(
"SELECT cache_hit FROM llm_calls WHERE call_id = ?", (kwargs["call_id"],)
).fetchone()
conn.close()
assert row["cache_hit"] == 1
@pytest.mark.asyncio
async def test_duplicate_call_id_does_not_raise(recorder, db_path):
"""重复 call_id 写入应静默忽略(INSERT OR IGNORE),不抛异常。"""
kwargs = _make_call_kwargs()
await recorder.record_llm_call(**kwargs)
await recorder.record_llm_call(**kwargs)
conn = sqlite3.connect(str(db_path))
rows = conn.execute("SELECT COUNT(*) FROM llm_calls").fetchone()
conn.close()
assert rows[0] == 1
@pytest.mark.asyncio
async def test_db_error_does_not_propagate(tmp_path):
"""SQLite 写入失败时 record_llm_call 应静默降级,不抛异常。"""
bad_recorder = SQLiteTelemetryRecorder(db_path=tmp_path / "nonexistent_dir" / "bad.db")
kwargs = _make_call_kwargs()
await bad_recorder.record_llm_call(**kwargs)
@pytest.mark.asyncio
async def test_concurrent_writes_no_lock_error(recorder, db_path):
"""16 路并发 record_llm_call 应全部成功,无 database is locked 错误。"""
import asyncio
tasks = []
for _ in range(16):
kwargs = _make_call_kwargs()
tasks.append(recorder.record_llm_call(**kwargs))
await asyncio.gather(*tasks)
conn = sqlite3.connect(str(db_path))
count = conn.execute("SELECT COUNT(*) FROM llm_calls").fetchone()[0]
conn.close()
assert count == 16
def test_high_concurrency_writes_zero_loss(recorder, db_path):
"""64 路线程并发直压 _write 应零丢失——复现生产 concurrency 下 database is locked 丢失。
直接压同步 _write(不经 to_thread 排队),最大化并发连接数以逼出锁竞争;
单连接 + threading.Lock 串行化模式下应全部落库、零丢失(对齐 HarnessLog)。
"""
import concurrent.futures
n = 64
kwargs_list = [_make_call_kwargs() for _ in range(n)]
with concurrent.futures.ThreadPoolExecutor(max_workers=n) as executor:
list(executor.map(lambda kw: recorder._write(**kw), kwargs_list))
conn = sqlite3.connect(str(db_path))
count = conn.execute("SELECT COUNT(*) FROM llm_calls").fetchone()[0]
conn.close()
assert count == n, f"并发写丢失 {n - count} 条(database is locked 降级丢弃): 落库 {count}/{n}"
def test_uses_single_persistent_connection(db_path, monkeypatch):
"""对齐 HarnessLog:单持久连接(构造时建一次),写入复用而非每次新建。
每次写新建连接是并发锁竞争根源(多连接争 SQLite 写锁,撑爆 busy_timeout);
单连接 + 进程内 Lock 串行化把并发控制拉到进程内,消除 SQLite 层锁竞争。
"""
connect_calls = {"n": 0}
real_connect = sqlite3.connect
def _counting_connect(*args, **kwargs):
connect_calls["n"] += 1
return real_connect(*args, **kwargs)
monkeypatch.setattr(sqlite3, "connect", _counting_connect)
recorder = SQLiteTelemetryRecorder(db_path=db_path)
after_init = connect_calls["n"]
for _ in range(10):
recorder._write(**_make_call_kwargs())
after_writes = connect_calls["n"]
assert after_init >= 1, "构造时应建立持久连接(对齐 HarnessLog"
assert after_writes == after_init, (
f"写入期间新建了 {after_writes - after_init} 个连接(应复用单持久连接,"
"每次新连接是并发锁竞争根源)"
)