feat: 保存当前使用的 Android 设备 (#22)
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""当前使用 Android 设备的本地设置服务。"""
|
||||
|
||||
from .settings_repository import SettingsRepository
|
||||
|
||||
|
||||
SELECTED_ANDROID_SERIAL_KEY = "android.selected_serial"
|
||||
MAX_ANDROID_SERIAL_LENGTH = 255
|
||||
|
||||
|
||||
class SelectedAndroidDeviceService:
|
||||
"""只保存自动化应使用的设备号,不缓存 ADB 搜索结果。"""
|
||||
|
||||
def __init__(self, repository: SettingsRepository):
|
||||
self._repository = repository
|
||||
|
||||
def load(self) -> str:
|
||||
"""读取已保存设备号;缺失或损坏时返回空字符串。"""
|
||||
|
||||
value = self._repository.get(SELECTED_ANDROID_SERIAL_KEY, "")
|
||||
if not isinstance(value, str):
|
||||
return ""
|
||||
return value.strip()
|
||||
|
||||
def save(self, serial: str) -> str:
|
||||
"""校验并保存设备号,返回规范化后的值。"""
|
||||
|
||||
normalized = self._normalize_serial(serial)
|
||||
self._repository.set(SELECTED_ANDROID_SERIAL_KEY, normalized)
|
||||
return normalized
|
||||
|
||||
def delete(self) -> bool:
|
||||
"""清除当前设备配置;配置不存在时也视为安全完成。"""
|
||||
|
||||
return self._repository.delete(SELECTED_ANDROID_SERIAL_KEY)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_serial(serial: object) -> str:
|
||||
if not isinstance(serial, str):
|
||||
raise ValueError("Android 设备号必须是文本")
|
||||
|
||||
normalized = serial.strip()
|
||||
if not normalized:
|
||||
raise ValueError("Android 设备号不能为空")
|
||||
if len(normalized) > MAX_ANDROID_SERIAL_LENGTH:
|
||||
raise ValueError(
|
||||
f"Android 设备号最多 {MAX_ANDROID_SERIAL_LENGTH} 个字符"
|
||||
)
|
||||
if any(character.isspace() for character in normalized):
|
||||
raise ValueError("Android 设备号不能包含空白字符")
|
||||
return normalized
|
||||
+238
-16
@@ -1,7 +1,7 @@
|
||||
"""设置页事件、ADB 搜索、当前 Client 本地保存和后台登记。
|
||||
|
||||
本文件不直接写 SQL。ADB、SQLite 保存和 Admin HTTP 请求由 Worker 在线程中
|
||||
执行,后台结果只通过信号返回主线程更新页面。
|
||||
本文件不直接写 SQL。ADB 搜索、SQLite 写入和 Admin HTTP 请求由 Worker 在线程中
|
||||
执行;页面启动时只同步读取少量索引设置,后台结果通过信号返回主线程更新页面。
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
@@ -24,6 +24,7 @@ from .current_client_service import (
|
||||
generate_client_device_id,
|
||||
)
|
||||
from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway
|
||||
from .selected_android_device_service import SelectedAndroidDeviceService
|
||||
from .settings_repository import SettingsRepository
|
||||
from .settings_ui import AndroidDeviceRow
|
||||
|
||||
@@ -129,6 +130,54 @@ class AndroidDeviceSearchWorker(QObject):
|
||||
self.completed.emit()
|
||||
|
||||
|
||||
class AndroidDeviceSettingWorker(QObject):
|
||||
"""在后台保存或删除当前使用的 Android 设备号。"""
|
||||
|
||||
succeeded = pyqtSignal(str, str)
|
||||
failed = pyqtSignal(str, str)
|
||||
completed = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service: SelectedAndroidDeviceService,
|
||||
action: str,
|
||||
serial: str = "",
|
||||
):
|
||||
super().__init__()
|
||||
self._service = service
|
||||
self._action = action
|
||||
self._serial = serial
|
||||
self._cancelled = False
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""写入无法中断;完成后不再向已关闭页面返回结果。"""
|
||||
|
||||
self._cancelled = True
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self) -> None:
|
||||
try:
|
||||
try:
|
||||
if self._action == "save":
|
||||
serial = self._service.save(self._serial)
|
||||
elif self._action == "delete":
|
||||
self._service.delete()
|
||||
serial = ""
|
||||
else:
|
||||
raise ValueError("未知的 Android 设备设置操作")
|
||||
except Exception as exc:
|
||||
if not self._cancelled:
|
||||
self.failed.emit(
|
||||
self._action,
|
||||
str(exc) or "无法写入本地数据库",
|
||||
)
|
||||
else:
|
||||
if not self._cancelled:
|
||||
self.succeeded.emit(self._action, serial)
|
||||
finally:
|
||||
self.completed.emit()
|
||||
|
||||
|
||||
class SettingsPageEventBinder(QObject):
|
||||
"""绑定设备管理控件,并向应用层发出稳定事件。"""
|
||||
|
||||
@@ -155,12 +204,19 @@ class SettingsPageEventBinder(QObject):
|
||||
self._search_busy = False
|
||||
self._search_thread: Optional[QThread] = None
|
||||
self._search_worker: Optional[AndroidDeviceSearchWorker] = None
|
||||
self._android_setting_busy = False
|
||||
self._android_setting_thread: Optional[QThread] = None
|
||||
self._android_setting_worker: Optional[AndroidDeviceSettingWorker] = None
|
||||
self._saved_android_serial = ""
|
||||
self._android_device_service = (
|
||||
android_device_service or AndroidDeviceService()
|
||||
)
|
||||
|
||||
repository = settings_repository or SettingsRepository()
|
||||
self._client_service = CurrentClientService(repository)
|
||||
self._selected_android_device_service = SelectedAndroidDeviceService(
|
||||
repository
|
||||
)
|
||||
self._admin_gateway = admin_gateway
|
||||
self._gateway_error = ""
|
||||
if self._admin_gateway is None:
|
||||
@@ -185,7 +241,9 @@ class SettingsPageEventBinder(QObject):
|
||||
self.searchRequested.connect(self._start_search)
|
||||
page.connectButton.clicked.connect(self._request_connect)
|
||||
page.saveButton.clicked.connect(self._request_save)
|
||||
self.saveRequested.connect(self._start_save_android_device)
|
||||
page.deleteButton.clicked.connect(self._request_delete)
|
||||
self.deleteRequested.connect(self._start_delete_android_device)
|
||||
page.addressInput.textChanged.connect(self._sync_button_state)
|
||||
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
|
||||
page.destroyed.connect(self.shutdown)
|
||||
@@ -194,11 +252,17 @@ class SettingsPageEventBinder(QObject):
|
||||
application.aboutToQuit.connect(self.shutdown)
|
||||
|
||||
self._load_current_client()
|
||||
self._load_selected_android_device()
|
||||
self._sync_button_state()
|
||||
|
||||
@pyqtSlot()
|
||||
def _request_save_current_device(self) -> None:
|
||||
if self._busy or self._current_device_busy or self._search_busy:
|
||||
if (
|
||||
self._busy
|
||||
or self._current_device_busy
|
||||
or self._search_busy
|
||||
or self._android_setting_busy
|
||||
):
|
||||
return
|
||||
|
||||
device_id = self._page.deviceIdInput.text().strip()
|
||||
@@ -210,7 +274,12 @@ class SettingsPageEventBinder(QObject):
|
||||
def _start_save_current_device(
|
||||
self, _displayed_device_id: str, device_name: str
|
||||
) -> None:
|
||||
if self._closing or self._current_device_busy or self._search_busy:
|
||||
if (
|
||||
self._closing
|
||||
or self._current_device_busy
|
||||
or self._search_busy
|
||||
or self._android_setting_busy
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -250,7 +319,12 @@ class SettingsPageEventBinder(QObject):
|
||||
|
||||
@pyqtSlot()
|
||||
def _request_search(self) -> None:
|
||||
if self._busy or self._current_device_busy or self._search_busy:
|
||||
if (
|
||||
self._busy
|
||||
or self._current_device_busy
|
||||
or self._search_busy
|
||||
or self._android_setting_busy
|
||||
):
|
||||
return
|
||||
self.searchRequested.emit()
|
||||
|
||||
@@ -261,6 +335,7 @@ class SettingsPageEventBinder(QObject):
|
||||
or self._busy
|
||||
or self._current_device_busy
|
||||
or self._search_busy
|
||||
or self._android_setting_busy
|
||||
):
|
||||
return
|
||||
|
||||
@@ -300,7 +375,7 @@ class SettingsPageEventBinder(QObject):
|
||||
|
||||
@pyqtSlot()
|
||||
def _request_save(self) -> None:
|
||||
if self._busy:
|
||||
if self._busy or self._search_busy or self._android_setting_busy:
|
||||
return
|
||||
|
||||
serial = self._page.deviceTableModel.checked_serial
|
||||
@@ -309,21 +384,69 @@ class SettingsPageEventBinder(QObject):
|
||||
self._page.deviceTable.setFocus()
|
||||
return
|
||||
|
||||
device_name = self._page.deviceNameInput.text().strip()
|
||||
self.saveRequested.emit(device_name, serial)
|
||||
self.saveRequested.emit("", serial)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _start_save_android_device(
|
||||
self, _unused_device_name: str, serial: str
|
||||
) -> None:
|
||||
self._start_android_device_setting("save", serial)
|
||||
|
||||
@pyqtSlot()
|
||||
def _request_delete(self) -> None:
|
||||
if self._busy:
|
||||
if self._busy or self._search_busy or self._android_setting_busy:
|
||||
return
|
||||
|
||||
serial = self._page.deviceTableModel.checked_serial
|
||||
serial = self._saved_android_serial
|
||||
if not serial:
|
||||
self._page.deviceStatusLabel.setText("删除失败:请先勾选一台设备")
|
||||
self._page.deviceTable.setFocus()
|
||||
self._page.deviceStatusLabel.setText("删除失败:当前没有已保存设备")
|
||||
return
|
||||
self.deleteRequested.emit(serial)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _start_delete_android_device(self, _serial: str) -> None:
|
||||
self._start_android_device_setting("delete")
|
||||
|
||||
def _start_android_device_setting(
|
||||
self, action: str, serial: str = ""
|
||||
) -> None:
|
||||
if (
|
||||
self._closing
|
||||
or self._busy
|
||||
or self._search_busy
|
||||
or self._android_setting_busy
|
||||
):
|
||||
return
|
||||
|
||||
self._android_setting_busy = True
|
||||
self._sync_button_state()
|
||||
message = (
|
||||
"正在保存当前使用设备…"
|
||||
if action == "save"
|
||||
else "正在删除已保存设备配置…"
|
||||
)
|
||||
self._page.deviceStatusLabel.setText(message)
|
||||
|
||||
thread = QThread(self)
|
||||
worker = AndroidDeviceSettingWorker(
|
||||
self._selected_android_device_service,
|
||||
action,
|
||||
serial,
|
||||
)
|
||||
worker.moveToThread(thread)
|
||||
|
||||
thread.started.connect(worker.run)
|
||||
worker.succeeded.connect(self._on_android_device_setting_succeeded)
|
||||
worker.failed.connect(self._on_android_device_setting_failed)
|
||||
worker.completed.connect(thread.quit)
|
||||
worker.completed.connect(worker.deleteLater)
|
||||
thread.finished.connect(self._on_android_device_setting_finished)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
|
||||
self._android_setting_thread = thread
|
||||
self._android_setting_worker = worker
|
||||
thread.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def _sync_button_state(self, *_args) -> None:
|
||||
serial = self._page.deviceTableModel.checked_serial
|
||||
@@ -332,8 +455,13 @@ class SettingsPageEventBinder(QObject):
|
||||
not self._busy
|
||||
and not self._current_device_busy
|
||||
and not self._search_busy
|
||||
and not self._android_setting_busy
|
||||
)
|
||||
device_commands_enabled = (
|
||||
not self._busy
|
||||
and not self._search_busy
|
||||
and not self._android_setting_busy
|
||||
)
|
||||
device_commands_enabled = not self._busy and not self._search_busy
|
||||
self._page.searchButton.setEnabled(
|
||||
device_commands_enabled and not self._current_device_busy
|
||||
)
|
||||
@@ -344,7 +472,7 @@ class SettingsPageEventBinder(QObject):
|
||||
device_commands_enabled and bool(serial)
|
||||
)
|
||||
self._page.deleteButton.setEnabled(
|
||||
device_commands_enabled and bool(serial)
|
||||
device_commands_enabled and bool(self._saved_android_serial)
|
||||
)
|
||||
|
||||
def set_busy(self, busy: bool, message: str = "") -> None:
|
||||
@@ -370,6 +498,23 @@ class SettingsPageEventBinder(QObject):
|
||||
if saved.client_id:
|
||||
self._page.set_current_device_status("本地设备信息已加载")
|
||||
|
||||
def _load_selected_android_device(self) -> None:
|
||||
"""页面创建时恢复自动化应使用的 Android 设备号。"""
|
||||
|
||||
try:
|
||||
serial = self._selected_android_device_service.load()
|
||||
except Exception as exc:
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"读取已保存 Android 设备失败:{str(exc) or '数据库不可用'}"
|
||||
)
|
||||
return
|
||||
|
||||
self._saved_android_serial = serial
|
||||
if serial:
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"当前使用设备:{serial}(等待搜索确认连接)"
|
||||
)
|
||||
|
||||
def _selected_android_device(self) -> Optional[AndroidDeviceInfo]:
|
||||
serial = self._page.deviceTableModel.checked_serial.strip()
|
||||
return AndroidDeviceInfo(serial) if serial else None
|
||||
@@ -390,7 +535,23 @@ class SettingsPageEventBinder(QObject):
|
||||
for device in devices
|
||||
]
|
||||
self._page.set_android_devices(rows)
|
||||
if rows:
|
||||
saved_device = self._page.deviceTableModel.device_for_serial(
|
||||
self._saved_android_serial
|
||||
)
|
||||
if saved_device is not None and saved_device.status == "device":
|
||||
self._page.deviceTableModel.set_checked_serial(
|
||||
self._saved_android_serial
|
||||
)
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"搜索完成:找到 {len(rows)} 台 Android 设备;"
|
||||
"已自动选择当前使用设备"
|
||||
)
|
||||
elif self._saved_android_serial:
|
||||
self._page.deviceStatusLabel.setText(
|
||||
"已保存设备当前未连接:"
|
||||
f"{self._saved_android_serial};配置已保留"
|
||||
)
|
||||
elif rows:
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"搜索完成:找到 {len(rows)} 台 Android 设备"
|
||||
)
|
||||
@@ -404,7 +565,7 @@ class SettingsPageEventBinder(QObject):
|
||||
if self._closing:
|
||||
return
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"搜索失败:{message};设备列表未更改"
|
||||
f"搜索失败:{message};设备列表和已保存配置未更改"
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -415,6 +576,44 @@ class SettingsPageEventBinder(QObject):
|
||||
if not self._closing:
|
||||
self._sync_button_state()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _on_android_device_setting_succeeded(
|
||||
self, action: str, serial: str
|
||||
) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
|
||||
if action == "save":
|
||||
self._saved_android_serial = serial
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"当前使用设备:{serial}(已保存)"
|
||||
)
|
||||
else:
|
||||
self._saved_android_serial = ""
|
||||
self._page.deviceTableModel.set_checked_serial("")
|
||||
self._page.deviceStatusLabel.setText(
|
||||
"当前使用设备:未选择(本地配置已删除)"
|
||||
)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _on_android_device_setting_failed(
|
||||
self, action: str, message: str
|
||||
) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
action_text = "保存" if action == "save" else "删除"
|
||||
self._page.deviceStatusLabel.setText(
|
||||
f"{action_text}失败:{message};原设备配置未更改,可重试"
|
||||
)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_android_device_setting_finished(self) -> None:
|
||||
self._android_setting_worker = None
|
||||
self._android_setting_thread = None
|
||||
self._android_setting_busy = False
|
||||
if not self._closing:
|
||||
self._sync_button_state()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _on_local_saved(self, client_id: str, client_name: str) -> None:
|
||||
if self._closing:
|
||||
@@ -495,3 +694,26 @@ class SettingsPageEventBinder(QObject):
|
||||
if search_thread is not None and search_thread.isRunning():
|
||||
search_thread.quit()
|
||||
search_thread.wait(6000)
|
||||
|
||||
setting_worker = self._android_setting_worker
|
||||
setting_thread = self._android_setting_thread
|
||||
if setting_worker is not None:
|
||||
setting_worker.cancel()
|
||||
for signal, slot in (
|
||||
(
|
||||
setting_worker.succeeded,
|
||||
self._on_android_device_setting_succeeded,
|
||||
),
|
||||
(
|
||||
setting_worker.failed,
|
||||
self._on_android_device_setting_failed,
|
||||
),
|
||||
):
|
||||
try:
|
||||
signal.disconnect(slot)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if setting_thread is not None and setting_thread.isRunning():
|
||||
setting_thread.quit()
|
||||
setting_thread.wait(4000)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""当前使用 Android 设备设置服务测试。"""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.selected_android_device_service import (
|
||||
SELECTED_ANDROID_SERIAL_KEY,
|
||||
SelectedAndroidDeviceService,
|
||||
)
|
||||
from src.settings_repository import SettingsRepository
|
||||
|
||||
|
||||
class SelectedAndroidDeviceServiceTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_directory = tempfile.TemporaryDirectory()
|
||||
db_path = Path(self.temp_directory.name) / "client.db"
|
||||
self.repository = SettingsRepository(db_path)
|
||||
self.service = SelectedAndroidDeviceService(self.repository)
|
||||
|
||||
def tearDown(self):
|
||||
self.temp_directory.cleanup()
|
||||
|
||||
def test_missing_setting_loads_as_empty(self):
|
||||
self.assertEqual(self.service.load(), "")
|
||||
|
||||
def test_save_normalizes_and_persists_serial(self):
|
||||
serial = self.service.save(" 192.168.0.173:5555 ")
|
||||
|
||||
self.assertEqual(serial, "192.168.0.173:5555")
|
||||
self.assertEqual(
|
||||
self.repository.get(SELECTED_ANDROID_SERIAL_KEY), serial
|
||||
)
|
||||
|
||||
def test_invalid_serial_is_rejected(self):
|
||||
for serial in ("", "USB 001", None):
|
||||
with self.subTest(serial=serial):
|
||||
with self.assertRaises(ValueError):
|
||||
self.service.save(serial)
|
||||
|
||||
def test_corrupted_setting_loads_as_empty(self):
|
||||
self.repository.set(SELECTED_ANDROID_SERIAL_KEY, {"serial": "bad"})
|
||||
|
||||
self.assertEqual(self.service.load(), "")
|
||||
|
||||
def test_delete_removes_only_selected_serial(self):
|
||||
self.repository.set("android.pdd_package", "com.xunmeng.pinduoduo")
|
||||
self.service.save("USB-001")
|
||||
|
||||
self.assertTrue(self.service.delete())
|
||||
|
||||
self.assertIsNone(self.repository.get(SELECTED_ANDROID_SERIAL_KEY))
|
||||
self.assertEqual(
|
||||
self.repository.get("android.pdd_package"),
|
||||
"com.xunmeng.pinduoduo",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -17,6 +17,7 @@ from src.android_device_service import (
|
||||
AndroidDeviceSearchError,
|
||||
)
|
||||
from src.mock_admin_gateway import MockAdminGateway
|
||||
from src.selected_android_device_service import SELECTED_ANDROID_SERIAL_KEY
|
||||
from src.settings_repository import SettingsRepository
|
||||
from src.settings_ui import AndroidDeviceRow, SettingsPage
|
||||
|
||||
@@ -57,6 +58,34 @@ class SlowAndroidDeviceService:
|
||||
return [AndroidDevice("USB-001", "USB", "Pixel 8", "14")]
|
||||
|
||||
|
||||
class ControlledSettingsRepository(SettingsRepository):
|
||||
"""控制 Android 设备设置写入速度和失败,其他设置保持正常。"""
|
||||
|
||||
def __init__(self, db_path, delay=0.0):
|
||||
super().__init__(db_path)
|
||||
self.delay = delay
|
||||
self.fail_selected_write = False
|
||||
self.selected_write_count = 0
|
||||
|
||||
def set(self, setting_key, value, updated_at=None):
|
||||
if setting_key == SELECTED_ANDROID_SERIAL_KEY:
|
||||
self.selected_write_count += 1
|
||||
if self.delay:
|
||||
time.sleep(self.delay)
|
||||
if self.fail_selected_write:
|
||||
raise OSError("模拟数据库写入失败")
|
||||
return super().set(setting_key, value, updated_at)
|
||||
|
||||
def delete(self, setting_key):
|
||||
if setting_key == SELECTED_ANDROID_SERIAL_KEY:
|
||||
self.selected_write_count += 1
|
||||
if self.delay:
|
||||
time.sleep(self.delay)
|
||||
if self.fail_selected_write:
|
||||
raise OSError("模拟数据库删除失败")
|
||||
return super().delete(setting_key)
|
||||
|
||||
|
||||
class SettingsPageEventTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -254,7 +283,7 @@ class SettingsPageEventTest(unittest.TestCase):
|
||||
page.deviceTableModel.device_for_serial("USB-OLD")
|
||||
)
|
||||
self.assertIn("搜索失败:未找到 adb", page.deviceStatusLabel.text())
|
||||
self.assertIn("设备列表未更改", page.deviceStatusLabel.text())
|
||||
self.assertIn("已保存配置未更改", page.deviceStatusLabel.text())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
@@ -321,6 +350,186 @@ class SettingsPageEventTest(unittest.TestCase):
|
||||
)
|
||||
page.deleteLater()
|
||||
|
||||
def test_selected_android_device_is_saved_to_sqlite(self):
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
page.set_android_devices(
|
||||
[AndroidDeviceRow("USB-001", "USB", "Pixel 8", "14")]
|
||||
)
|
||||
page.deviceTableModel.set_checked_serial("USB-001")
|
||||
|
||||
page.saveButton.click()
|
||||
self.assertFalse(page.saveButton.isEnabled())
|
||||
self._wait_until(
|
||||
lambda: page.eventBinder._android_setting_thread is None
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-001"
|
||||
)
|
||||
self.assertEqual(
|
||||
page.deviceStatusLabel.text(),
|
||||
"当前使用设备:USB-001(已保存)",
|
||||
)
|
||||
self.assertTrue(page.deleteButton.isEnabled())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_saved_android_device_is_restored_before_search(self):
|
||||
self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001")
|
||||
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
|
||||
self.assertEqual(page.eventBinder._saved_android_serial, "USB-001")
|
||||
self.assertEqual(page.deviceTableModel.checked_serial, "")
|
||||
self.assertIn("等待搜索确认连接", page.deviceStatusLabel.text())
|
||||
self.assertTrue(page.deleteButton.isEnabled())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_search_auto_selects_saved_online_device(self):
|
||||
self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001")
|
||||
service = SequenceAndroidDeviceService(
|
||||
[[AndroidDevice("USB-001", "USB", "Pixel 8", "14")]]
|
||||
)
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
android_device_service=service,
|
||||
)
|
||||
|
||||
page.searchButton.click()
|
||||
self._wait_until(lambda: page.eventBinder._search_thread is None)
|
||||
|
||||
self.assertEqual(page.deviceTableModel.checked_serial, "USB-001")
|
||||
self.assertIn("已自动选择", page.deviceStatusLabel.text())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_search_keeps_saved_device_when_it_is_offline(self):
|
||||
self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-001")
|
||||
service = SequenceAndroidDeviceService([[]])
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
android_device_service=service,
|
||||
)
|
||||
|
||||
page.searchButton.click()
|
||||
self._wait_until(lambda: page.eventBinder._search_thread is None)
|
||||
|
||||
self.assertEqual(
|
||||
self.repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-001"
|
||||
)
|
||||
self.assertEqual(page.deviceTableModel.checked_serial, "")
|
||||
self.assertIn("已保存设备当前未连接", page.deviceStatusLabel.text())
|
||||
self.assertIn("配置已保留", page.deviceStatusLabel.text())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_selected_android_device_save_failure_keeps_old_configuration(self):
|
||||
repository = ControlledSettingsRepository(self.db_path)
|
||||
repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-OLD")
|
||||
repository.fail_selected_write = True
|
||||
page = SettingsPage(
|
||||
settings_repository=repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
page.set_android_devices(
|
||||
[
|
||||
AndroidDeviceRow("USB-OLD", "USB"),
|
||||
AndroidDeviceRow("USB-NEW", "USB"),
|
||||
]
|
||||
)
|
||||
page.deviceTableModel.set_checked_serial("USB-NEW")
|
||||
|
||||
page.saveButton.click()
|
||||
self._wait_until(
|
||||
lambda: page.eventBinder._android_setting_thread is None
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
repository.get(SELECTED_ANDROID_SERIAL_KEY), "USB-OLD"
|
||||
)
|
||||
self.assertEqual(page.deviceTableModel.checked_serial, "USB-NEW")
|
||||
self.assertIn("原设备配置未更改", page.deviceStatusLabel.text())
|
||||
self.assertTrue(page.saveButton.isEnabled())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_delete_clears_saved_offline_device_only(self):
|
||||
self.repository.set(SELECTED_ANDROID_SERIAL_KEY, "USB-OFFLINE")
|
||||
self.repository.set("android.pdd_package", "com.xunmeng.pinduoduo")
|
||||
page = SettingsPage(
|
||||
settings_repository=self.repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
|
||||
page.deleteButton.click()
|
||||
self._wait_until(
|
||||
lambda: page.eventBinder._android_setting_thread is None
|
||||
)
|
||||
|
||||
self.assertIsNone(
|
||||
self.repository.get(SELECTED_ANDROID_SERIAL_KEY)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.repository.get("android.pdd_package"),
|
||||
"com.xunmeng.pinduoduo",
|
||||
)
|
||||
self.assertIn("本地配置已删除", page.deviceStatusLabel.text())
|
||||
self.assertFalse(page.deleteButton.isEnabled())
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_slow_android_device_save_does_not_block_or_start_twice(self):
|
||||
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
||||
page = SettingsPage(
|
||||
settings_repository=repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||
page.deviceTableModel.set_checked_serial("USB-001")
|
||||
timer_fired = []
|
||||
QTimer.singleShot(10, lambda: timer_fired.append(True))
|
||||
|
||||
page.saveButton.click()
|
||||
page.eventBinder._request_save()
|
||||
self._wait_until(lambda: bool(timer_fired), timeout_ms=500)
|
||||
self._wait_until(
|
||||
lambda: page.eventBinder._android_setting_thread is None
|
||||
)
|
||||
|
||||
self.assertTrue(timer_fired)
|
||||
self.assertEqual(repository.selected_write_count, 1)
|
||||
page.eventBinder.shutdown()
|
||||
page.deleteLater()
|
||||
|
||||
def test_shutdown_ignores_late_android_device_save_result(self):
|
||||
repository = ControlledSettingsRepository(self.db_path, delay=0.08)
|
||||
page = SettingsPage(
|
||||
settings_repository=repository,
|
||||
admin_gateway=MockAdminGateway(),
|
||||
)
|
||||
page.set_android_devices([AndroidDeviceRow("USB-001", "USB")])
|
||||
page.deviceTableModel.set_checked_serial("USB-001")
|
||||
|
||||
page.saveButton.click()
|
||||
self._wait_until(
|
||||
lambda: page.eventBinder._android_setting_thread is not None
|
||||
)
|
||||
status_before_close = page.deviceStatusLabel.text()
|
||||
page.eventBinder.shutdown()
|
||||
QTest.qWait(120)
|
||||
|
||||
self.assertEqual(page.deviceStatusLabel.text(), status_before_close)
|
||||
page.deleteLater()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user