from types import SimpleNamespace from unittest.mock import MagicMock import pytest from artemis.drivers.android.adb_driver import AndroidAdbDriver from artemis.drivers.errors import AppLaunchError PACKAGE = "com.example.app" COMPONENT = f"{PACKAGE}/.MainActivity" def response(output, code=0): return SimpleNamespace(returncode=code, output=output) @pytest.fixture def launch_device(): device = MagicMock() # An emulator without monkey must work, without installing or invoking it. device.shell.side_effect = AssertionError("Unexpected unchecked shell/monkey call") driver = AndroidAdbDriver("test-device", MagicMock()) driver._device = device return driver, device @pytest.mark.asyncio @pytest.mark.parametrize( "output", [ "Starting: Intent {}\nStatus: ok\nComplete", "Starting: Intent {}\nWarning: Activity not started, its current task has been brought to the front", "Warning: Activity not started, intent has been delivered to currently running top-most instance.", ], ) async def test_launch_without_monkey_checks_start_result(launch_device, output): driver, device = launch_device device.shell2.side_effect = [response(f"priority=0\n{COMPONENT}"), response(output)] assert await driver.launch_app(PACKAGE) is True calls = device.shell2.call_args_list assert calls[0].args[0][:3] == ["cmd", "package", "resolve-activity"] assert calls[1].args[0][-2:] == ["-n", COMPONENT] assert calls[1].args[0][:5] == ["am", "start", "-W", "--user", "current"] assert "0x10200000" in calls[1].args[0] assert all(call.kwargs["timeout"] > 0 for call in calls) device.shell.assert_not_called() @pytest.mark.asyncio async def test_falls_back_to_pm_when_cmd_is_missing(launch_device): driver, device = launch_device device.shell2.side_effect = [ response("/system/bin/sh: cmd: inaccessible or not found", 127), response(COMPONENT), response("Status: ok"), ] assert await driver.launch_app(PACKAGE) is True assert device.shell2.call_args_list[1].args[0][:2] == ["pm", "resolve-activity"] @pytest.mark.asyncio async def test_old_resolvers_use_package_scoped_intent(launch_device): driver, device = launch_device device.shell2.side_effect = [ response("Unknown command: resolve-activity", 1), response("Unknown command: resolve-activity", 1), response("Status: ok"), ] assert await driver.launch_app(PACKAGE) is True assert device.shell2.call_args_list[-1].args[0][-2:] == ["-p", PACKAGE] @pytest.mark.asyncio @pytest.mark.parametrize("output", ["No activity found", "", "android/.ResolverActivity"]) async def test_missing_or_ambiguous_launcher_does_not_start(launch_device, output): driver, device = launch_device device.shell2.return_value = response(output) with pytest.raises(AppLaunchError, match="未找到唯一可启动入口") as error: await driver.launch_app(PACKAGE) assert error.value.retryable is False assert device.shell2.call_count == 1 @pytest.mark.asyncio @pytest.mark.parametrize( "output, code", [ ("Starting: Intent {}\nError type 3\nError: Activity class does not exist.", 0), ("java.lang.SecurityException: Permission Denial: not exported", 0), ("Error: Permission Denial", 1), ("/system/bin/sh: am: inaccessible or not found", 127), ("Starting: Intent {}\nStatus: error", 0), ], ) async def test_start_errors_are_retained_even_when_exit_zero(launch_device, output, code): driver, device = launch_device device.shell2.side_effect = [response(COMPONENT), response(output, code)] with pytest.raises(AppLaunchError) as error: await driver.launch_app(PACKAGE) assert output in str(error.value) assert f"退出码 {code}" in str(error.value) assert error.value.retryable is False assert error.value.may_have_started is False @pytest.mark.asyncio @pytest.mark.parametrize("failure", [TimeoutError(), response("Status: timeout"), response("")]) async def test_unknown_start_outcome_requires_foreground_check(launch_device, failure): driver, device = launch_device device.shell2.side_effect = [response(COMPONENT), failure] with pytest.raises(AppLaunchError) as error: await driver.launch_app(PACKAGE) assert error.value.retryable is True assert error.value.may_have_started is True @pytest.mark.asyncio async def test_resolve_transport_failure_does_not_request_a_launch(launch_device): driver, device = launch_device device.shell2.side_effect = OSError("device offline") with pytest.raises(AppLaunchError, match="device offline") as error: await driver.launch_app(PACKAGE) assert error.value.retryable is True assert error.value.may_have_started is False assert device.shell2.call_count == 1 @pytest.mark.asyncio @pytest.mark.parametrize( "package", ["", "com.example.app;reboot", "com.example.app/.Activity", "a b"] ) async def test_invalid_package_rejected_before_adb(launch_device, package): driver, device = launch_device with pytest.raises(AppLaunchError, match="无效"): await driver.launch_app(package) device.shell2.assert_not_called()