Let 1-day view growth use a zero baseline for videos published today, and the first successful sample for older videos ingested today, without mixing those into 7/30-day rankings.
143 lines
4.7 KiB
Python
143 lines
4.7 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 == 8
|
|
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 == 8
|
|
assert db2.read(lambda c: c.execute("SELECT MAX(version) FROM schema_version").fetchone()[0]) == 8
|
|
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, 3, 4, 5, 6, 7, 8]
|
|
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()
|