Backfill same-day snapshots for newly added or restored videos, preserve successful fan snapshots on resume, persist incomplete state before forced re-runs, and split collection completion from backup completion so failed backups can retry without API calls. Reject missing data payloads instead of treating them as empty folders, guard against mass-unfav on suspicious empty remote folders, rebuild Bilibili clients when credentials change, recompute primary folder ownership from active relations with per-folder fav_time, preserve original titles for dropped invalid videos, poll config every 60 seconds, and fix favsort verify interval and mixed resource types.
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""数据库单写队列测试(不联网): uv run python tests/test_database.py"""
|
|
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
from reaction_board.database import Database, DatabaseClosed # noqa: E402
|
|
|
|
|
|
def make_db(tmp):
|
|
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
|
|
db.initialize()
|
|
return db
|
|
|
|
|
|
def test_write_read_transaction():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db = make_db(tmp)
|
|
try:
|
|
assert db.schema_version == 2
|
|
db.write(lambda c: c.execute("INSERT INTO kv(k, v) VALUES ('a', '1')"))
|
|
assert db.read(lambda c: c.execute("SELECT v FROM kv WHERE k='a'").fetchone()[0]) == "1"
|
|
|
|
def boom(conn):
|
|
conn.execute("UPDATE kv SET v='2' WHERE k='a'")
|
|
raise RuntimeError("boom")
|
|
try:
|
|
db.transaction(boom)
|
|
except RuntimeError as exc:
|
|
assert str(exc) == "boom"
|
|
else:
|
|
raise AssertionError("事务应抛错")
|
|
assert db.read(lambda c: c.execute("SELECT v FROM kv WHERE k='a'").fetchone()[0]) == "1"
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_concurrent_writes_are_serialized():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db = make_db(tmp)
|
|
try:
|
|
db.write(lambda c: c.execute(
|
|
"CREATE TABLE counter (id INTEGER PRIMARY KEY, n INTEGER NOT NULL)"
|
|
))
|
|
db.write(lambda c: c.execute("INSERT INTO counter(id, n) VALUES (1, 0)"))
|
|
errors = []
|
|
|
|
def worker():
|
|
try:
|
|
for _ in range(10):
|
|
db.transaction(
|
|
lambda conn: conn.execute(
|
|
"UPDATE counter SET n = n + 1 WHERE id = 1"
|
|
)
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
errors.append(exc)
|
|
|
|
threads = [threading.Thread(target=worker) for _ in range(8)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
assert not errors, errors
|
|
assert db.read(lambda c: c.execute("SELECT n FROM counter").fetchone()[0]) == 80
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_close_and_reinitialize():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db = make_db(tmp)
|
|
db.close()
|
|
db.close() # 幂等
|
|
try:
|
|
db.write(lambda c: None)
|
|
except DatabaseClosed:
|
|
pass
|
|
else:
|
|
raise AssertionError("关闭后写入应失败")
|
|
try:
|
|
db.read(lambda c: None)
|
|
except DatabaseClosed:
|
|
pass
|
|
else:
|
|
raise AssertionError("关闭后读取应失败")
|
|
|
|
db2 = make_db(tmp)
|
|
try:
|
|
assert db2.schema_version == 2
|
|
assert db2.read(lambda c: c.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]) == 2
|
|
finally:
|
|
db2.close()
|
|
|
|
|
|
def test_wal_enabled():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db = make_db(tmp)
|
|
try:
|
|
mode = db.read(lambda c: c.execute("PRAGMA journal_mode").fetchone()[0])
|
|
assert str(mode).lower() == "wal"
|
|
assert db.read(lambda c: c.execute("PRAGMA foreign_keys").fetchone()[0]) == 1
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_migration_backup_on_existing_db():
|
|
"""已有数据升级时由 Database 自动备份。"""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = Path(tmp) / "app.db"
|
|
# 先建一个旧库(只有 schema_version v0),Database.initialize 会升级并备份
|
|
conn = sqlite3.connect(path)
|
|
conn.execute("CREATE TABLE legacy (x INTEGER)")
|
|
conn.commit()
|
|
conn.close()
|
|
db = Database(path, backup_dir=Path(tmp) / "backup")
|
|
try:
|
|
applied = db.initialize()
|
|
assert applied == [1, 2]
|
|
assert len(list((Path(tmp) / "backup").glob("app-pre-v0-*.db"))) == 1
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def main():
|
|
test_write_read_transaction()
|
|
test_concurrent_writes_are_serialized()
|
|
test_close_and_reinitialize()
|
|
test_wal_enabled()
|
|
test_migration_backup_on_existing_db()
|
|
print("OK: 数据库单写队列与迁移入口测试全部通过")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|