feat: 支持持续自动获取任务 (#45)
This commit is contained in:
@@ -12,7 +12,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from src.pdd_ui import PDDTaskPage
|
||||
from src.admin_gateway import AdminTask, SubmissionReceipt
|
||||
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
|
||||
from src.pdd_ui_event import (
|
||||
PDDTaskPageEvent,
|
||||
admin_task_to_new_claimed_task,
|
||||
@@ -78,6 +78,35 @@ class SlowClaimGateway(RecordingClaimGateway):
|
||||
return super().claim_next(client, capabilities)
|
||||
|
||||
|
||||
class SequenceClaimGateway(RecordingClaimGateway):
|
||||
"""依次返回多条任务,并记录是否出现并发领取。"""
|
||||
|
||||
def __init__(self, responses):
|
||||
super().__init__()
|
||||
self.responses = list(responses)
|
||||
self.active_calls = 0
|
||||
self.max_active_calls = 0
|
||||
|
||||
def claim_next(self, client, capabilities):
|
||||
self.active_calls += 1
|
||||
self.max_active_calls = max(self.max_active_calls, self.active_calls)
|
||||
try:
|
||||
time.sleep(0.01)
|
||||
response = self.responses.pop(0) if self.responses else None
|
||||
self.response = response
|
||||
return super().claim_next(client, capabilities)
|
||||
finally:
|
||||
self.active_calls -= 1
|
||||
|
||||
|
||||
class RetryableClaimGateway(RecordingClaimGateway):
|
||||
"""模拟 Admin 暂时不可用。"""
|
||||
|
||||
def claim_next(self, client, capabilities):
|
||||
self.calls.append((client, capabilities))
|
||||
raise AdminGatewayError("ADMIN_UNAVAILABLE", "Admin 暂时不可用", True)
|
||||
|
||||
|
||||
class FakeCollectResult:
|
||||
def to_pdd_data(self):
|
||||
return {
|
||||
@@ -331,7 +360,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
"737116531267",
|
||||
)
|
||||
|
||||
def test_click_claims_one_collect_task_saves_and_refreshes_table(self):
|
||||
def test_start_claims_task_and_keeps_auto_fetch_running(self):
|
||||
gateway = RecordingClaimGateway(collect_admin_task())
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
@@ -357,12 +386,12 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.assertEqual(self.repository.count_tasks(), 1)
|
||||
self.assertEqual(page.taskModel.data_row_count(), 1)
|
||||
self.assertEqual(page.taskModel.row_at(0).remote_task_id, "COL-001")
|
||||
self.assertEqual(page.autoFetchButton.text(), "获取任务")
|
||||
self.assertEqual(page.autoFetchButton.text(), "停止自动获取")
|
||||
self.assertIn("COL-001", page.statusLabel.text())
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_204_is_neutral_and_each_click_only_calls_once(self):
|
||||
def test_no_task_schedules_next_claim_and_stop_cancels_timer(self):
|
||||
gateway = RecordingClaimGateway(None)
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
@@ -377,12 +406,18 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(len(gateway.calls), 1)
|
||||
self.assertEqual(page.statusLabel.text(), "暂无可领取的采集任务")
|
||||
self.assertIn("5 秒后再次领取", page.statusLabel.text())
|
||||
self.assertTrue(events._next_cycle_timer.isActive())
|
||||
self.assertEqual(self.repository.count_tasks(), 0)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertFalse(events._next_cycle_timer.isActive())
|
||||
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_repeated_click_while_claiming_does_not_start_second_request(self):
|
||||
def test_second_click_while_claiming_requests_safe_stop(self):
|
||||
gateway = SlowClaimGateway(None)
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
@@ -394,11 +429,86 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
self.assertTrue(wait_until(self.app, gateway.started.is_set))
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertFalse(page.autoFetchButton.isEnabled())
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(len(gateway.calls), 1)
|
||||
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_one_start_processes_multiple_tasks_strictly_in_sequence(self):
|
||||
gateway = SequenceClaimGateway(
|
||||
[collect_admin_task("COL-001"), collect_admin_task("COL-002")]
|
||||
)
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
next_task_delay_ms=10,
|
||||
no_task_delay_ms=1_000,
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertTrue(
|
||||
wait_until(self.app, lambda: self.repository.count_tasks() == 2)
|
||||
)
|
||||
self.assertEqual(gateway.max_active_calls, 1)
|
||||
self.assertEqual(page.autoFetchButton.text(), "停止自动获取")
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_retryable_admin_error_schedules_backoff(self):
|
||||
gateway = RetryableClaimGateway()
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
retry_delays_ms=(20, 40, 80, 100),
|
||||
)
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(events._retry_count, 1)
|
||||
self.assertEqual(events._next_cycle_timer.interval(), 20)
|
||||
self.assertIn("退避等待", page.statusLabel.text())
|
||||
|
||||
page.autoFetchRequested.emit()
|
||||
self.assertFalse(events._next_cycle_timer.isActive())
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_retry_delay_grows_to_cap_and_success_resets_it(self):
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=RecordingClaimGateway(None),
|
||||
settings_repository=self._saved_settings(),
|
||||
retry_delays_ms=(5_000, 10_000, 20_000, 30_000),
|
||||
)
|
||||
|
||||
observed = []
|
||||
for _ in range(5):
|
||||
events._on_claim_retryable_failed("Admin 暂时不可用")
|
||||
observed.append(events._cycle_next_delay_ms)
|
||||
|
||||
self.assertEqual(observed, [5_000, 10_000, 20_000, 30_000, 30_000])
|
||||
events._on_collect_outcome("succeeded", "任务完成", "COL-001")
|
||||
self.assertEqual(events._retry_count, 0)
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
@@ -459,9 +569,34 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(gateway.calls, [])
|
||||
self.assertIn("设置页", page.statusLabel.text())
|
||||
self.assertFalse(events._auto_fetch_running)
|
||||
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_shutdown_cancels_scheduled_next_claim(self):
|
||||
gateway = RecordingClaimGateway(None)
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
no_task_delay_ms=20,
|
||||
)
|
||||
page.autoFetchRequested.emit()
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertTrue(events._next_cycle_timer.isActive())
|
||||
|
||||
events.shutdown()
|
||||
deadline = time.monotonic() + 0.08
|
||||
while time.monotonic() < deadline:
|
||||
self.app.processEvents()
|
||||
time.sleep(0.005)
|
||||
|
||||
self.assertEqual(len(gateway.calls), 1)
|
||||
page.deleteLater()
|
||||
|
||||
def test_shutdown_after_claim_started_still_saves_task_without_ui_callback(self):
|
||||
gateway = SlowClaimGateway(collect_admin_task("COL-CLOSE"))
|
||||
page = PDDTaskPage()
|
||||
|
||||
Reference in New Issue
Block a user