feat(p0): add favorite folder sync and sqlite data foundation
Implement P0 of the reaction dashboard: config loading with hot reload, SQLite migrations with backups, single-writer queue, read-only Bilibili fav client, folder import/diff sync, invalid-resource handling, blacklist seed, scheduler, and JSON redacted logging. Includes offline unit and transport-level contract tests; all existing and new tests pass.
This commit is contained in:
commit
e2cdf997af
39 files changed
+8575
No files matched your search
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""内置调度器测试(离线): uv run python tests/test_scheduler.py"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from reaction_board.scheduler import ScheduledTask, Scheduler # noqa: E402
|
||||
|
||||
|
||||
def test_run_when_due_and_reschedule():
|
||||
clock = [1_000.0]
|
||||
calls = []
|
||||
|
||||
task = ScheduledTask("sync", 60, lambda: calls.append(clock[0]))
|
||||
scheduler = Scheduler([task], clock=lambda: clock[0])
|
||||
assert scheduler.run_due() and len(calls) == 1
|
||||
assert task.next_run == 1_060.0
|
||||
|
||||
clock[0] = 1_059.0
|
||||
assert scheduler.run_due() == []
|
||||
assert len(calls) == 1
|
||||
|
||||
clock[0] = 1_060.0
|
||||
results = scheduler.run_due()
|
||||
assert len(results) == 1
|
||||
assert calls == [1_000.0, 1_060.0]
|
||||
assert task.next_run == 1_120.0
|
||||
|
||||
|
||||
def test_task_error_does_not_block_other_tasks():
|
||||
clock = [100.0]
|
||||
calls = []
|
||||
|
||||
def boom():
|
||||
raise RuntimeError("api down")
|
||||
|
||||
scheduler = Scheduler(
|
||||
[
|
||||
ScheduledTask("bad", 10, boom),
|
||||
ScheduledTask("good", 10, lambda: calls.append("ok")),
|
||||
],
|
||||
clock=lambda: clock[0],
|
||||
)
|
||||
results = scheduler.run_due()
|
||||
assert [r.ok for r in results] == [False, True]
|
||||
assert results[0].error == "api down"
|
||||
assert calls == ["ok"]
|
||||
# 失败任务仍然被重新排期
|
||||
assert all(t.next_run == 110.0 for t in scheduler.tasks)
|
||||
|
||||
|
||||
def test_run_forever_with_stop_event():
|
||||
clock = [0.0]
|
||||
calls = []
|
||||
|
||||
def tick():
|
||||
calls.append(clock[0])
|
||||
clock[0] += 10
|
||||
|
||||
stop = threading.Event()
|
||||
scheduler = Scheduler(
|
||||
[ScheduledTask("tick", 10, tick)],
|
||||
clock=lambda: clock[0],
|
||||
sleeper=lambda seconds: None,
|
||||
)
|
||||
|
||||
def stop_soon():
|
||||
while len(calls) < 3:
|
||||
pass
|
||||
stop.set()
|
||||
|
||||
waiter = threading.Thread(target=stop_soon)
|
||||
waiter.start()
|
||||
scheduler.run_forever(stop_event=stop, poll_seconds=0.001)
|
||||
waiter.join(timeout=2)
|
||||
assert len(calls) >= 3
|
||||
|
||||
|
||||
def test_max_iterations_and_validation():
|
||||
calls = []
|
||||
tick_clock = [0.0]
|
||||
|
||||
def next_time():
|
||||
tick_clock[0] += 1.0
|
||||
return tick_clock[0]
|
||||
|
||||
scheduler = Scheduler(
|
||||
[ScheduledTask("t", 1, lambda: calls.append(1))],
|
||||
clock=next_time,
|
||||
sleeper=lambda seconds: None,
|
||||
)
|
||||
scheduler.run_forever(max_iterations=2)
|
||||
assert len(calls) == 2
|
||||
|
||||
for tasks, kwargs in [
|
||||
([], {}),
|
||||
([ScheduledTask("bad", 0, lambda: None)], {}),
|
||||
([ScheduledTask("t", 1, lambda: None)], {"poll_seconds": 0}),
|
||||
]:
|
||||
try:
|
||||
s = Scheduler(tasks, sleeper=lambda seconds: None)
|
||||
s.run_forever(max_iterations=1, **kwargs)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError(f"应当失败: {tasks}, {kwargs}")
|
||||
|
||||
|
||||
def main():
|
||||
test_run_when_due_and_reschedule()
|
||||
test_task_error_does_not_block_other_tasks()
|
||||
test_run_forever_with_stop_event()
|
||||
test_max_iterations_and_validation()
|
||||
print("OK: 内置调度器测试全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user