feat: 内置并统一使用项目 ADB (#149)

This commit is contained in:
chengma
2026-08-11 12:01:07 +08:00
parent 56b1a46c5c
commit 8f2037db2f
21 changed files with 355 additions and 46 deletions
+50
View File
@@ -0,0 +1,50 @@
"""项目内置 ADB 路径与完整性检查测试。"""
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from src.adb_runtime import (
BundledAdbError,
REQUIRED_ADB_FILES,
bundled_adb_path,
configure_bundled_adb_environment,
validate_bundled_adb,
)
class BundledAdbRuntimeTest(unittest.TestCase):
def test_project_adb_is_complete(self):
adb_path = validate_bundled_adb()
self.assertEqual(adb_path, bundled_adb_path())
self.assertTrue(adb_path.is_absolute())
for file_name in REQUIRED_ADB_FILES:
self.assertTrue((adb_path.parent / file_name).is_file())
def test_missing_file_reports_name_without_path_fallback(self):
with tempfile.TemporaryDirectory() as directory, patch(
"src.adb_runtime.app_dir", return_value=Path(directory)
):
with self.assertRaisesRegex(BundledAdbError, "adb.exe"):
validate_bundled_adb()
def test_configure_forces_adbutils_to_use_project_adb(self):
old_value = os.environ.get("ADBUTILS_ADB_PATH")
try:
configured = configure_bundled_adb_environment()
self.assertEqual(
os.environ["ADBUTILS_ADB_PATH"],
str(configured),
)
finally:
if old_value is None:
os.environ.pop("ADBUTILS_ADB_PATH", None)
else:
os.environ["ADBUTILS_ADB_PATH"] = old_value
if __name__ == "__main__":
unittest.main()
+23 -19
View File
@@ -3,12 +3,16 @@
import subprocess
import unittest
from src.adb_runtime import bundled_adb_path
from src.android_device_service import (
AndroidDeviceSearchError,
AndroidDeviceService,
)
ADB = str(bundled_adb_path())
def completed(command, stdout="", stderr="", returncode=0):
"""创建一条假的 ADB 命令结果。"""
@@ -56,7 +60,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
commands.append(list(command))
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -111,12 +115,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
command = list(command)
commands.append(command)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(
command,
"already connected to 192.168.0.173:5555\n",
)
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -131,7 +135,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(devices[0].serial, "192.168.0.173:5555")
self.assertEqual(
commands[0],
["adb", "connect", "192.168.0.173:5555"],
[ADB, "connect", "192.168.0.173:5555"],
)
def test_restore_saved_wifi_waits_for_authorization_handshake(self):
@@ -141,9 +145,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
nonlocal device_list_count
command = list(command)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
device_list_count += 1
status = "unauthorized" if device_list_count == 1 else "device"
return completed(
@@ -167,7 +171,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(command, _timeout):
command = list(command)
commands.append(command)
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -181,12 +185,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(devices[0].serial, "USB-001")
self.assertFalse(
any(command[:2] == ["adb", "connect"] for command in commands)
any(command[:2] == [ADB, "connect"] for command in commands)
)
def test_missing_model_is_read_from_device(self):
def runner(command, _timeout):
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\nUSB-001 device transport_id:1\n",
@@ -202,7 +206,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def test_property_failure_keeps_discovered_device(self):
def runner(command, _timeout):
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\nUSB-001 device model:Phone\n",
@@ -218,7 +222,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
def runner(_command, _timeout):
raise FileNotFoundError("adb")
with self.assertRaisesRegex(AndroidDeviceSearchError, "未找到 adb"):
with self.assertRaisesRegex(AndroidDeviceSearchError, "内置 adb.exe"):
AndroidDeviceService(runner).search()
def test_timeout_has_recovery_message(self):
@@ -253,7 +257,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
self.assertEqual(
commands,
[[
"adb",
ADB,
"-s",
"USB-001",
"shell",
@@ -296,9 +300,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
"192.168.0.0/24 dev wlan0 scope link "
"src 192.168.0.173\n",
)
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
@@ -323,14 +327,14 @@ USB-002 unauthorized usb:1-2 transport_id:3
["USB-001", "192.168.0.173:5555"],
)
self.assertIn(
["adb", "-s", "USB-001", "tcpip", "5555"],
[ADB, "-s", "USB-001", "tcpip", "5555"],
commands,
)
self.assertEqual(
commands.count(["adb", "connect", "192.168.0.173:5555"]),
commands.count([ADB, "connect", "192.168.0.173:5555"]),
1,
)
self.assertEqual(commands.count(["adb", "devices", "-l"]), 3)
self.assertEqual(commands.count([ADB, "devices", "-l"]), 3)
self.assertTrue(
any("保存后拔掉 USB" in message for message in progress)
)
@@ -346,9 +350,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
command = list(command)
if command[-3:] == ["shell", "ip", "route"]:
return completed(command, "local src 192.168.0.173\n")
if command[:2] == ["adb", "connect"]:
if command[:2] == [ADB, "connect"]:
return completed(command, "connected to 192.168.0.173:5555\n")
if command == ["adb", "devices", "-l"]:
if command == [ADB, "devices", "-l"]:
return completed(
command,
"List of devices attached\n"
+21 -1
View File
@@ -21,7 +21,7 @@ from build_tools.release_manifest import (
versioned_manifest_file_name,
write_manifest,
)
from src.db import data_dir
from src.db import app_dir, data_dir
class LauncherPathTest(unittest.TestCase):
@@ -139,6 +139,26 @@ class PackagedDataPathTest(unittest.TestCase):
):
self.assertEqual(data_dir(), root / "data")
def test_packaged_resource_path_uses_pyinstaller_directory(self):
with tempfile.TemporaryDirectory() as directory, patch(
"src.db.sys.frozen", True, create=True
), patch("src.db.sys._MEIPASS", directory, create=True):
self.assertEqual(app_dir(), Path(directory).resolve())
class BundledAdbPackagingTest(unittest.TestCase):
def test_build_script_packages_and_checks_all_adb_files(self):
client_root = Path(__file__).resolve().parents[1]
script = (client_root / "packaging" / "build.ps1").read_text(
encoding="utf-8-sig"
)
self.assertIn("--add-binary", script)
self.assertIn("vendor/android-platform-tools/windows", script)
self.assertIn("adb.exe", script)
self.assertIn("AdbWinApi.dll", script)
self.assertIn("AdbWinUsbApi.dll", script)
class ReleaseManifestTest(unittest.TestCase):
def test_manifest_name_and_hash_match_release_files(self):
+35
View File
@@ -14,6 +14,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from qfluentwidgets import InfoBarPosition
from src.adb_runtime import BundledAdbError
from src.android_device_service import AndroidDeviceSearchError
from src.db import open_database
from src.pdd_ui import PDDTaskPage
@@ -1174,6 +1175,40 @@ class PDDTaskPageEventTest(unittest.TestCase):
self.assertEqual(result, 0)
self.assertTrue(window.maximized)
def test_application_startup_shows_dialog_when_bundled_adb_is_missing(self):
class FakeApplication:
@staticmethod
def setAttribute(*_args):
pass
def __init__(self, _args):
pass
def exec_(self):
return 0
class FakeWindow:
def showMaximized(self):
pass
window = FakeWindow()
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
with patch("src.ui_main.QApplication", FakeApplication), patch(
"src.ui_main.MainWindow", return_value=window
), patch("src.ui_main.setTheme"), patch(
"src.ui_main.mark_current_version_healthy"
), patch(
"src.ui_main.configure_bundled_adb_environment",
side_effect=BundledAdbError(message),
), patch(
"src.ui_main.QTimer.singleShot",
side_effect=lambda _delay, callback: callback(),
), patch("src.ui_main._show_adb_missing_dialog") as show_dialog:
result = ui_main()
self.assertEqual(result, 0)
show_dialog.assert_called_once_with(window, message)
def test_main_window_marks_and_clears_update_navigation(self):
window = MainWindow(
task_repository=self.repository,
+20
View File
@@ -5,6 +5,7 @@ import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
@@ -12,6 +13,7 @@ from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtTest import QTest
from PyQt5.QtWidgets import QApplication
from src.adb_runtime import BundledAdbError
from src.android_device_service import (
AndroidDevice,
AndroidDeviceConversionCancelled,
@@ -218,6 +220,24 @@ class SettingsPageEventTest(unittest.TestCase):
page.eventBinder.shutdown()
page.deleteLater()
def test_missing_bundled_adb_disables_device_commands(self):
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
with patch(
"src.settings_ui_event.validate_bundled_adb",
side_effect=BundledAdbError(message),
):
page = SettingsPage(
settings_repository=self.repository,
admin_gateway=MockAdminGateway(),
)
self.assertEqual(page.deviceStatusLabel.text(), message)
self.assertFalse(page.searchButton.isEnabled())
self.assertFalse(page.convertWifiButton.isEnabled())
self.assertTrue(page.currentDeviceSaveButton.isEnabled())
page.eventBinder.shutdown()
page.deleteLater()
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
page = SettingsPage(
settings_repository=self.repository,