返回 douyin-downloader
test_queue_manager.py
根目录 / tests / test_queue_manager.py
1 import asyncio
2 import time
3
4 import pytest
5
6 from control.queue_manager import QueueManager
7
8
9 @pytest.mark.asyncio
10 async def test_process_tasks_returns_results_in_order():
11 qm = QueueManager(max_workers=3)
12
13 def make_task(value):
14 async def _task():
15 return value
16
17 return _task
18
19 tasks = [make_task(i) for i in range(5)]
20 results = await qm.process_tasks(tasks)
21 assert results == [0, 1, 2, 3, 4]
22
23
24 @pytest.mark.asyncio
25 async def test_process_tasks_surfaces_exceptions_without_killing_others():
26 # The previous implementation swallowed exceptions and returned None,
27 # making "task succeeded with None result" indistinguishable from
28 # "task threw". Failures must surface with the original exception attached.
29 qm = QueueManager(max_workers=3)
30
31 async def succeed():
32 return "ok"
33
34 async def boom():
35 raise RuntimeError("kapow")
36
37 tasks = [succeed, boom, succeed]
38 results = await qm.process_tasks(tasks)
39
40 assert results[0] == "ok"
41 assert isinstance(results[1], RuntimeError)
42 assert str(results[1]) == "kapow"
43 assert results[2] == "ok"
44
45
46 @pytest.mark.asyncio
47 async def test_process_tasks_respects_concurrency_cap():
48 qm = QueueManager(max_workers=2)
49 in_flight = 0
50 peak = 0
51
52 async def slow():
53 nonlocal in_flight, peak
54 in_flight += 1
55 peak = max(peak, in_flight)
56 await asyncio.sleep(0.05)
57 in_flight -= 1
58 return "done"
59
60 await qm.process_tasks([slow] * 8)
61 assert peak == 2, f"peak concurrency was {peak}, expected 2"
62
63
64 @pytest.mark.asyncio
65 async def test_download_batch_returns_results_in_order():
66 qm = QueueManager(max_workers=3)
67
68 async def echo(item):
69 return {"status": "success", "item": item}
70
71 items = ["a", "b", "c"]
72 results = await qm.download_batch(echo, items)
73 assert [r["item"] for r in results] == ["a", "b", "c"]
74 assert all(r["status"] == "success" for r in results)
75
76
77 @pytest.mark.asyncio
78 async def test_download_batch_surfaces_exceptions_alongside_successes():
79 # When one item raises, the other items must still complete and the
80 # exception must be observable in the results list.
81 qm = QueueManager(max_workers=3)
82
83 async def maybe_fail(item):
84 if item == "bad":
85 raise ValueError(f"bad item: {item}")
86 return {"status": "success", "item": item}
87
88 results = await qm.download_batch(maybe_fail, ["a", "bad", "c"])
89
90 assert isinstance(results[0], dict) and results[0]["status"] == "success"
91 assert isinstance(results[1], ValueError)
92 assert "bad item: bad" in str(results[1])
93 assert isinstance(results[2], dict) and results[2]["status"] == "success"
94
95
96 @pytest.mark.asyncio
97 async def test_download_batch_respects_concurrency_cap():
98 qm = QueueManager(max_workers=2)
99 in_flight = 0
100 peak = 0
101
102 async def slow(_item):
103 nonlocal in_flight, peak
104 in_flight += 1
105 peak = max(peak, in_flight)
106 await asyncio.sleep(0.05)
107 in_flight -= 1
108 return {"status": "success"}
109
110 start = time.time()
111 await qm.download_batch(slow, list(range(8)))
112 elapsed = time.time() - start
113
114 assert peak == 2, f"peak concurrency was {peak}, expected 2"
115 # 8 tasks at concurrency 2, 0.05s each = 4 batches = 0.2s minimum
116 assert elapsed >= 0.18, f"elapsed {elapsed:.3f}s suggests concurrency cap not enforced"
117
117 lines PYTHON