fix: 任务执行前检查 Android 设备连接 (#84)
This commit is contained in:
@@ -72,6 +72,39 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
||||
self.assertEqual(len(commands), 2)
|
||||
self.assertIn("ro.build.version.release", commands[1])
|
||||
|
||||
def test_require_connected_accepts_exact_usb_and_wifi_serial(self):
|
||||
output = (
|
||||
"List of devices attached\n"
|
||||
"USB-001 device model:Phone\n"
|
||||
"192.168.0.173:5555 device model:Phone\n"
|
||||
)
|
||||
service = AndroidDeviceService(
|
||||
lambda command, _timeout: completed(command, output)
|
||||
)
|
||||
|
||||
usb = service.require_connected("USB-001")
|
||||
wifi = service.require_connected("192.168.0.173:5555")
|
||||
|
||||
self.assertEqual(usb.serial, "USB-001")
|
||||
self.assertEqual(wifi.serial, "192.168.0.173:5555")
|
||||
|
||||
def test_require_connected_rejects_missing_offline_and_unauthorized(self):
|
||||
cases = (
|
||||
("USB-MISSING", "", "USB.*未连接"),
|
||||
("192.168.0.173:5555", "offline", "offline"),
|
||||
("USB-002", "unauthorized", "未授权"),
|
||||
)
|
||||
for serial, status, expected in cases:
|
||||
with self.subTest(serial=serial, status=status):
|
||||
line = f"{serial} {status}\n" if status else ""
|
||||
service = AndroidDeviceService(
|
||||
lambda command, _timeout, value=line: completed(
|
||||
command, "List of devices attached\n" + value
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(AndroidDeviceSearchError, expected):
|
||||
service.require_connected(serial)
|
||||
|
||||
def test_restore_saved_wifi_connects_before_search(self):
|
||||
commands = []
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from src.android_device_service import AndroidDeviceSearchError
|
||||
from src.pdd_ui import PDDTaskPage
|
||||
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
|
||||
from src.pdd_ui_event import (
|
||||
@@ -238,8 +239,14 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
self.temp_directory = tempfile.TemporaryDirectory()
|
||||
self.db_path = Path(self.temp_directory.name) / "client.db"
|
||||
self.repository = TaskRepository(self.db_path)
|
||||
self.device_checker_patch = patch(
|
||||
"src.pdd_ui_event.AndroidDeviceService.require_connected",
|
||||
autospec=True,
|
||||
)
|
||||
self.device_checker_patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.device_checker_patch.stop()
|
||||
self.temp_directory.cleanup()
|
||||
|
||||
def _add_task(
|
||||
@@ -481,6 +488,97 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_disconnected_device_stops_auto_fetch_before_claim(self):
|
||||
page = PDDTaskPage()
|
||||
gateway = RecordingClaimGateway(collect_admin_task())
|
||||
checker_threads = []
|
||||
|
||||
def disconnected(_serial):
|
||||
checker_threads.append(threading.get_ident())
|
||||
raise AndroidDeviceSearchError("USB Android 设备 USB-001 未连接")
|
||||
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=gateway,
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
device_connection_checker=disconnected,
|
||||
)
|
||||
page.autoFetchRequested.emit()
|
||||
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
self.assertEqual(gateway.calls, [])
|
||||
self.assertEqual(self.repository.count_tasks(), 0)
|
||||
self.assertEqual(len(checker_threads), 1)
|
||||
self.assertNotEqual(checker_threads[0], threading.get_ident())
|
||||
self.assertIn("Android 设备不可用", page.statusLabel.text())
|
||||
self.assertIsNotNone(events._device_feedback)
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_disconnected_rerun_keeps_existing_task_unchanged(self):
|
||||
self._add_task(1)
|
||||
first = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||
event = self.repository.save_collect_result(
|
||||
"PDD-001", first.attempt_id, FakeCollectResult().to_pdd_data()
|
||||
)
|
||||
self.repository.mark_outbox_sent(event.id)
|
||||
before = self.repository.get_task("PDD-001")
|
||||
before_run = self.repository.latest_task_run("PDD-001")
|
||||
page = PDDTaskPage()
|
||||
def disconnected(_serial):
|
||||
raise AndroidDeviceSearchError(
|
||||
"USB Android 设备 USB-001 未连接"
|
||||
)
|
||||
|
||||
events = PDDTaskPageEvent(
|
||||
page,
|
||||
self.repository,
|
||||
claim_gateway=RecordingClaimGateway(),
|
||||
settings_repository=self._saved_settings(),
|
||||
collect_service_factory=fake_collect_factory,
|
||||
device_connection_checker=disconnected,
|
||||
)
|
||||
|
||||
with patch("src.pdd_ui_event.MessageBox") as message_box:
|
||||
message_box.return_value.exec.return_value = True
|
||||
page.rerunRequested.emit("PDD-001")
|
||||
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
|
||||
|
||||
after = self.repository.get_task("PDD-001")
|
||||
after_run = self.repository.latest_task_run("PDD-001")
|
||||
self.assertEqual(after.status, TaskStatus.SUCCEEDED)
|
||||
self.assertEqual(after.pdd_data, before.pdd_data)
|
||||
self.assertEqual(after_run.attempt_id, before_run.attempt_id)
|
||||
self.assertIn("重新采集未开始", page.statusLabel.text())
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_device_feedback_opens_settings_closes_and_does_not_stack(self):
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(page, self.repository)
|
||||
opened = []
|
||||
page.openSettingsRequested.connect(lambda: opened.append(True))
|
||||
|
||||
events._show_device_unavailable("第一条")
|
||||
first = events._device_feedback
|
||||
events._show_device_unavailable("第二条")
|
||||
second = events._device_feedback
|
||||
self.assertIsNot(first, second)
|
||||
buttons = {
|
||||
button.text(): button
|
||||
for button in second.findChildren(type(page.rerunButton))
|
||||
}
|
||||
self.assertIn("打开设置", buttons)
|
||||
self.assertIn("关闭提示", buttons)
|
||||
buttons["打开设置"].click()
|
||||
self.app.processEvents()
|
||||
self.assertEqual(opened, [True])
|
||||
self.assertIsNone(events._device_feedback)
|
||||
events.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_query_failure_shows_readable_error(self):
|
||||
page = PDDTaskPage()
|
||||
events = PDDTaskPageEvent(page, BrokenRepository())
|
||||
@@ -587,6 +685,9 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
||||
create_u2_purchase_adapter,
|
||||
)
|
||||
self.assertEqual(window.pddTaskPage.taskModel.data_row_count(), 0)
|
||||
window.pddTaskPage.openSettingsRequested.emit()
|
||||
self.app.processEvents()
|
||||
self.assertIs(window.stackedWidget.currentWidget(), window.settingsPage)
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
|
||||
@@ -235,6 +235,7 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
purchase_reconcile_factory=(
|
||||
lambda _address, _cancelled: ReadOnlyReconcileAdapter(calls)
|
||||
),
|
||||
device_connection_checker=lambda _serial: None,
|
||||
)
|
||||
|
||||
first = dispatcher.execute_one()
|
||||
@@ -267,6 +268,7 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
lambda _address, _cancelled: purchase_calls.append("purchase")
|
||||
),
|
||||
purchase_reconcile_factory=unavailable_reconcile,
|
||||
device_connection_checker=lambda _serial: None,
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(outcome.kind, "manual_review")
|
||||
|
||||
@@ -4,7 +4,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.admin_gateway import AdminTask, ClientInfo
|
||||
from src.android_device_service import AndroidDeviceSearchError
|
||||
from src.admin_gateway import AdminTask, ClientInfo, SubmissionReceipt
|
||||
from src.mock_admin_gateway import MockAdminGateway
|
||||
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
||||
from src.task_dispatcher import TaskDispatcher, admin_task_to_new_claimed_task
|
||||
@@ -102,7 +103,7 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def _dispatcher(self, *, purchase_ready):
|
||||
def _dispatcher(self, *, purchase_ready, device_checker=lambda _serial: None):
|
||||
purchase_factory = None
|
||||
if purchase_ready:
|
||||
purchase_factory = (
|
||||
@@ -117,6 +118,7 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
lambda *_args: RecordingCollector(self.calls)
|
||||
),
|
||||
purchase_adapter_factory=purchase_factory,
|
||||
device_connection_checker=device_checker,
|
||||
)
|
||||
|
||||
def test_capability_only_includes_purchase_when_adapter_is_ready(self):
|
||||
@@ -173,6 +175,56 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
assert detail is not None
|
||||
self.assertEqual(detail.status, TaskStatus.CLAIMED)
|
||||
|
||||
def test_disconnected_device_does_not_claim_new_task(self):
|
||||
self.gateway.enqueue_task(purchase_task(), self.client.client_id)
|
||||
|
||||
def disconnected(_serial):
|
||||
raise AndroidDeviceSearchError("USB Android 设备 USB-001 未连接")
|
||||
|
||||
with self.assertRaisesRegex(AndroidDeviceSearchError, "未连接"):
|
||||
self._dispatcher(
|
||||
purchase_ready=True, device_checker=disconnected
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(self.repository.count_tasks(), 0)
|
||||
|
||||
def test_pending_outbox_submits_without_connected_device(self):
|
||||
task = admin_task_to_new_claimed_task(
|
||||
AdminTask(
|
||||
"COL-OUTBOX",
|
||||
TaskType.COLLECT,
|
||||
1,
|
||||
0,
|
||||
{"goods_url": "https://example.test", "goods_id": "G"},
|
||||
)
|
||||
)
|
||||
self.repository.add_claimed_task(task)
|
||||
started = self.repository.start_collect_run("COL-OUTBOX", "USB-001")
|
||||
self.repository.save_collect_result(
|
||||
"COL-OUTBOX", started.attempt_id, FakeCollectResult().to_pdd_data()
|
||||
)
|
||||
checks = []
|
||||
|
||||
class AcceptGateway:
|
||||
def submit_result(self, *_args):
|
||||
return SubmissionReceipt(
|
||||
True, "RESULT-001", "2026-08-10T08:00:00Z"
|
||||
)
|
||||
|
||||
outcome = TaskDispatcher(
|
||||
AcceptGateway(),
|
||||
self.repository,
|
||||
self.client,
|
||||
"USB-001",
|
||||
purchase_adapter_factory=(
|
||||
lambda _address, _cancelled: ReadyPurchaseAdapter(self.calls)
|
||||
),
|
||||
device_connection_checker=lambda serial: checks.append(serial),
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(outcome.kind, "succeeded")
|
||||
self.assertEqual(checks, [])
|
||||
|
||||
def test_purchase_mapper_rejects_missing_or_invalid_safety_fields(self):
|
||||
invalid_payloads = (
|
||||
{"goods_url": "https://example.test"},
|
||||
|
||||
Reference in New Issue
Block a user