from __future__ import annotations from pathlib import Path import unittest from hibor_fast_collection.adb import AdbClient, UiNode from hibor_fast_collection.models import ContractError, ErrorCode from hibor_fast_collection.process import ProcessResult from hibor_fast_collection.ui import HiborUiDriver, UiConfig def result(exit_code: int = 0, *, stdout: bytes = b"", stderr: bytes = b"") -> ProcessResult: return ProcessResult( (), True, 1, exit_code, False, False, True, False, stdout, stderr, False, False, None, None, 1, None, ) class ScriptedSupervisor: def __init__(self, responses: list[ProcessResult]): self.responses = list(responses) self.calls: list[tuple[str, ...]] = [] def run(self, argv, *, timeout_ms, **kwargs): self.calls.append(tuple(argv)) if not self.responses: raise AssertionError(f"unexpected command: {argv!r}") return self.responses.pop(0) def client(responses: list[ProcessResult]) -> tuple[AdbClient, ScriptedSupervisor]: supervisor = ScriptedSupervisor(responses) return AdbClient(Path("adb.exe"), supervisor, serial="emulator-test"), supervisor class AdbInputCompatibilityTests(unittest.TestCase): def test_safe_ascii_uses_generic_input_without_clipboard_or_paste(self): adb, supervisor = client([result(), result(), result()]) adb.input_text("603179") commands = [call[3:] for call in supervisor.calls] self.assertEqual(commands[-1], ("shell", "input", "text", "603179")) self.assertFalse(any("memu_clip_board_base64" in call for call in supervisor.calls)) self.assertFalse(any(call[-2:] == ("keyevent", "279") for call in supervisor.calls)) def test_memu_service_failure_never_pastes(self): adb, supervisor = client([ result(stdout=b"com.microvirt.memuime/.MemuIME\n"), result(), result(), result(exit_code=1, stderr=b"service unavailable"), ]) with self.assertRaisesRegex(ContractError, "Memu clipboard failed"): adb.input_text("\u56fd\u74f7\u6750\u6599") self.assertTrue(any("memu_clip_board_base64" in call for call in supervisor.calls)) self.assertFalse(any(call[-2:] == ("keyevent", "279") for call in supervisor.calls)) def test_monkey_missing_falls_back_to_exported_welcome_activity(self): adb, supervisor = client([ result(exit_code=127, stderr=b"/system/bin/sh: monkey: not found"), result(stdout=b"Starting: Intent { cmp=cn.com.hibor/.WelcomeActivity }\nStatus: ok\n"), ]) adb.start_package() self.assertEqual( supervisor.calls[-1][3:], ("shell", "am", "start", "-W", "-n", "cn.com.hibor/.WelcomeActivity"), ) def test_unsupported_non_ascii_or_sensitive_input_fails_before_edit_or_paste(self): for value in ("\u4e09\u73af\u96c6\u56e2", "603179;paste"): with self.subTest(value=value): adb, supervisor = client([result(stdout=b"com.android.inputmethod.pinyin/.InputService\n")]) with self.assertRaisesRegex(ContractError, "MemuIME unsupported") as raised: adb.input_text(value) self.assertEqual(raised.exception.code, ErrorCode.UI_ANCHOR_DRIFT) self.assertEqual(len(supervisor.calls), 1) self.assertEqual(supervisor.calls[0][3:], ("shell", "ime", "list", "-s")) def test_ui_requires_exact_character_for_character_readback(self): field = UiNode("", "search-input", "android.widget.EditText", (0, 0, 10, 10), True) wrong = UiNode("603178", "search-input", "android.widget.EditText", (0, 0, 10, 10), True) class UiAdb: def __init__(self): self.snapshots = [(field,), (wrong,)] def ui_nodes(self): return self.snapshots.pop(0) def tap(self, *args): pass def input_text(self, value): self.value = value config = UiConfig((), "search-input", ("\u641c\u7d22",), "", (), "", "", "", "", (0, 0, 0, 0, 1)) driver = HiborUiDriver(UiAdb(), config) with self.assertRaisesRegex(ContractError, "text mismatch"): driver.search("603179") if __name__ == "__main__": unittest.main()