fix: 任务执行前检查 Android 设备连接 (#84)

This commit is contained in:
chengma
2026-08-10 09:35:46 +08:00
parent dee9f34035
commit e7fb06d7aa
11 changed files with 360 additions and 25 deletions
@@ -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 = []
+101
View File
@@ -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()
+2
View File
@@ -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")
+54 -2
View File
@@ -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"},