"""get_conn 连接策略契约:WAL 首连设置一次 + 目录自动创建。 背景:get_conn 原实现每个连接都执行 mkdir + PRAGMA journal_mode=WAL, 实测每次多付 ~1.2ms(mkdir 190us + 新连接上 PRAGMA ~1ms)。WAL 模式 持久化在库文件头里,与 core/db.py 的 _WAL_READY_FOR 同款,按路径设一次即可。 """ import sqlite3 from pathlib import Path from auth_system import db def _point_db_to(monkeypatch, tmp_path: Path, conn_counter: list[int] | None = None): """把 DB_PATH 指到临时库,并清空 WAL 已就绪缓存。""" new_path = tmp_path / 'site.db' monkeypatch.setattr(db, 'DB_PATH', new_path) monkeypatch.setattr(db, '_WAL_READY', set()) if conn_counter is not None: real_connect = sqlite3.connect def counting_connect(*a, **k): conn_counter[0] += 1 return real_connect(*a, **k) monkeypatch.setattr(db.sqlite3, 'connect', counting_connect) return new_path def test_wal_mode_active_on_fresh_db(monkeypatch, tmp_path): """新路径首连:WAL 生效。""" _point_db_to(monkeypatch, tmp_path) conn = db.get_conn() try: mode = conn.execute('PRAGMA journal_mode').fetchone()[0] assert mode.lower() == 'wal' finally: conn.close() def test_wal_pragma_only_once_per_path(monkeypatch, tmp_path): """同一路径第二次起不再执行 PRAGMA journal_mode=WAL(用 SQL 追踪器实证)。""" _point_db_to(monkeypatch, tmp_path) conn1 = db.get_conn() conn1.close() conn2 = db.get_conn() conn2.close() # 用 sqlite3 的 set_trace_callback 记录第三条连接上的语句(2 连接已建立缓存) statements: list[str] = [] conn3 = db.get_conn() conn3.set_trace_callback(statements.append) conn3.execute('SELECT 1').fetchall() conn3.close() assert statements == ['SELECT 1'], f'第三条连接不应再跑 PRAGMA,实际: {statements}' def test_new_path_resets_wal_cache(monkeypatch, tmp_path): """换库路径(测试隔离场景):新路径重新设置 WAL。""" _point_db_to(monkeypatch, tmp_path / 'a') c = db.get_conn() c.close() monkeypatch.setattr(db, 'DB_PATH', tmp_path / 'b' / 'site.db') c = db.get_conn() try: mode = c.execute('PRAGMA journal_mode').fetchone()[0] assert mode.lower() == 'wal' finally: c.close() def test_missing_dir_auto_created(monkeypatch, tmp_path): """目录不存在:connect 失败兜底 mkdir 重试,不经 init_db 也能用。""" deep = tmp_path / 'x' / 'y' _point_db_to(monkeypatch, deep) assert not deep.exists() conn = db.get_conn() try: conn.execute('CREATE TABLE t (v)').close() conn.commit() finally: conn.close() assert (deep / 'site.db').exists() def test_deleted_db_file_recovers(monkeypatch, tmp_path): """库文件被删(目录还在):重连自动重建且恢复 WAL 模式,不崩。""" _point_db_to(monkeypatch, tmp_path) c = db.get_conn() c.close() (tmp_path / 'site.db').unlink() conn = db.get_conn() try: conn.execute('SELECT 1').fetchall() mode = conn.execute('PRAGMA journal_mode').fetchone()[0] assert mode.lower() == 'wal', '重建的库文件必须恢复 WAL(journal_mode 不持久于新文件)' finally: conn.close() def test_concurrent_first_connect_no_lease_conflict(monkeypatch, tmp_path): """多线程同时首连(读路径不持 _DB_LOCK):WAL 设置正确、无连接报错。 core/db.py 记录过并发 PRAGMA 争排他锁报 database is locked 的实测坑, 同步层用专用锁 + 二次复查同款防护。 """ import threading _point_db_to(monkeypatch, tmp_path) errors: list[Exception] = [] def worker(): try: for _ in range(5): c = db.get_conn() c.execute('SELECT 1').fetchall() c.close() except Exception as e: # 收集全部失败供断言 errors.append(e) threads = [threading.Thread(target=worker) for _ in range(8)] for t in threads: t.start() for t in threads: t.join() assert errors == []