返回 MoneyPrinterTurbo
test_state.py
根目录 / test / services / test_state.py
1 import sys
2 import threading
3 import unittest
4 from pathlib import Path
5
6 sys.path.insert(0, str(Path(__file__).parent.parent.parent))
7
8 from app.models import const
9 from app.services.state import MemoryState, RedisState
10
11
12 class _FakeRedis:
13 def __init__(self, batches):
14 self.batches = batches
15 self.data = {}
16 for key in [key for batch in batches for key in batch]:
17 index = int(key.decode("utf-8").split(":")[-1])
18 self.data[key] = {
19 b"task_id": key,
20 b"state": b"1",
21 b"progress": str(index).encode("utf-8"),
22 }
23
24 def scan(self, cursor, count):
25 batch_index = int(cursor)
26 next_cursor = batch_index + 1
27 if next_cursor >= len(self.batches):
28 next_cursor = 0
29 return next_cursor, self.batches[batch_index]
30
31 def hgetall(self, key):
32 return self.data[key]
33
34
35 class TestMemoryState(unittest.TestCase):
36 def test_get_task_and_get_all_tasks_return_isolated_snapshots(self):
37 state = MemoryState()
38 state.update_task(
39 "task-1",
40 state=const.TASK_STATE_PROCESSING,
41 progress=25,
42 videos=["first.mp4"],
43 )
44
45 task = state.get_task("task-1")
46 task["videos"].append("mutated.mp4")
47
48 tasks, total = state.get_all_tasks(page=1, page_size=10)
49 tasks[0]["videos"].append("mutated-again.mp4")
50
51 self.assertEqual(total, 1)
52 self.assertEqual(state.get_task("task-1")["videos"], ["first.mp4"])
53
54 def test_concurrent_memory_updates_are_preserved(self):
55 state = MemoryState()
56 thread_count = 5
57 tasks_per_thread = 50
58
59 def update_tasks(thread_index):
60 for task_index in range(tasks_per_thread):
61 state.update_task(
62 f"task-{thread_index}-{task_index}",
63 state=const.TASK_STATE_PROCESSING,
64 progress=task_index,
65 )
66
67 threads = [
68 threading.Thread(target=update_tasks, args=(thread_index,))
69 for thread_index in range(thread_count)
70 ]
71 for thread in threads:
72 thread.start()
73 for thread in threads:
74 thread.join()
75
76 tasks, total = state.get_all_tasks(page=1, page_size=thread_count * tasks_per_thread)
77
78 self.assertEqual(total, thread_count * tasks_per_thread)
79 self.assertEqual(len(tasks), total)
80
81
82 class TestRedisState(unittest.TestCase):
83 def _build_state(self, batch_sizes):
84 keys = [f"task:{i}".encode("utf-8") for i in range(sum(batch_sizes))]
85 batches = []
86 offset = 0
87 for batch_size in batch_sizes:
88 batches.append(keys[offset : offset + batch_size])
89 offset += batch_size
90
91 state = RedisState.__new__(RedisState)
92 state._redis = _FakeRedis(batches)
93 return state
94
95 def test_get_all_tasks_paginates_across_scan_batches(self):
96 """
97 Redis SCAN 分批返回 key 时,分页切片必须按当前批次起始位置计算。
98
99 这个用例复现 PR #890 描述的 18 条任务、page_size=10 场景:
100 第一批 10 条,第二批 8 条。旧逻辑第一页会返回空列表,第二页
101 只返回 2 条;修复后第一页返回 10 条,第二页返回剩余 8 条。
102 """
103 state = self._build_state([10, 8])
104
105 first_page, first_total = state.get_all_tasks(page=1, page_size=10)
106 second_page, second_total = state.get_all_tasks(page=2, page_size=10)
107
108 self.assertEqual(first_total, 18)
109 self.assertEqual(second_total, 18)
110 self.assertEqual(len(first_page), 10)
111 self.assertEqual(len(second_page), 8)
112 self.assertEqual(
113 [task["task_id"] for task in first_page],
114 [f"task:{i}" for i in range(10)],
115 )
116 self.assertEqual(
117 [task["task_id"] for task in second_page],
118 [f"task:{i}" for i in range(10, 18)],
119 )
120
121
122 if __name__ == "__main__":
123 unittest.main()
124
124 lines PYTHON