From fd43f61bd6cc9c134323f1197d84f3c8ba44574d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:17:26 +0200 Subject: [PATCH 1/7] added "--forward-port" and "--reverse-port" options --- src/briefcase/integrations/android_sdk.py | 80 ++++++++++ src/briefcase/platforms/android/gradle.py | 143 +++++++++++------ .../android_sdk/ADB/test_forward_reverse.py | 117 ++++++++++++++ tests/platforms/android/gradle/test_run.py | 144 ++++++++++++++++++ 4 files changed, 440 insertions(+), 44 deletions(-) create mode 100644 tests/integrations/android_sdk/ADB/test_forward_reverse.py diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index e0ea3fe42..5d8942431 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1628,6 +1628,86 @@ def logcat_tail(self, since: datetime): except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting ADB logcat.") from e + def forward(self, host_port: int, device_port: int): + """Use the forward command to set up arbitrary port forwarding, which + forwards requests on a specific host port to a different port on a device. + + :param host_port: The port on the host that should be forwarded to the device + :param device_port: The port on the device + """ + try: + self.tools.subprocess.check_output( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "forward", + f"tcp:{host_port}", + f"tcp:{device_port}", + ], + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb forward'.") from e + + def forward_remove(self, host_port: int): + """Remove forwarded port. + + :param host_port: The port on the host that should be removed + """ + try: + self.tools.subprocess.check_output( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "forward", + "--remove", + f"tcp:{host_port}", + ], + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e + + def reverse(self, device_port: int, host_port: int): + """Use the reverse command to set up arbitrary port forwarding, which + forwards requests on a specific device port to a different port on the host. + + :param device_port: The port on the device that should be forwarded to the host + :param host_port: The port on the host + """ + try: + self.tools.subprocess.check_output( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "reverse", + f"tcp:{device_port}", + f"tcp:{host_port}", + ], + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb reverse'.") from e + + def reverse_remove(self, device_port: int): + """Remove reversed port. + + :param device_port: The port on the device that should be removed + """ + try: + self.tools.subprocess.check_output( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "reverse", + "--remove", + f"tcp:{device_port}", + ], + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e + def pidof(self, package: str, **kwargs) -> str | None: """Obtain the PID of a running app by package name. diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index d32f30a11..40844c1a2 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import datetime import re import subprocess @@ -18,7 +19,7 @@ from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF from briefcase.exceptions import BriefcaseCommandError -from briefcase.integrations.android_sdk import AndroidSDK +from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -358,6 +359,22 @@ def add_options(self, parser): help="Shutdown the emulator on exit", required=False, ) + parser.add_argument( + "--forward-port", + action="append", + dest="forward_ports", + type=int, + help="Forward the specified port from host to device.", + required=False, + ) + parser.add_argument( + "--reverse-port", + action="append", + dest="reverse_ports", + type=int, + help="Reverse the specified port from device to host.", + required=False, + ) def run_app( self, @@ -366,6 +383,8 @@ def run_app( device_or_avd=None, extra_emulator_args=None, shutdown_on_exit=False, + forward_ports: list[int] | None = None, + reverse_ports: list[int] | None = None, **kwargs, ): """Start the application. @@ -376,6 +395,8 @@ def run_app( be asked to re-run the command selecting a specific device. :param extra_emulator_args: Any additional arguments to pass to the emulator. :param shutdown_on_exit: Should the emulator be shut down on exit? + :param forward_ports: A list of ports to forward for the app. + :param reverse_ports: A list of ports to reversed for the app. """ device, name, avd = self.tools.android_sdk.select_target_device(device_or_avd) @@ -424,55 +445,89 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) - # To start the app, we launch `org.beeware.android.MainActivity`. - with self.console.wait_bar(f"Launching {label}..."): - # capture the earliest time for device logging in case PID not found - device_start_time = adb.datetime() - - adb.start_app(package, "org.beeware.android.MainActivity", passthrough) - - # Try to get the PID for 5 seconds. - pid = None - fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) - while not pid and datetime.datetime.now() < fail_time: - # Try to get the PID; run in quiet mode because we may - # need to do this a lot in the next 5 seconds. - pid = adb.pidof(package, quiet=2) - if not pid: - time.sleep(0.01) - - if pid: - self.console.info( - "Following device log output (type CTRL-C to stop log)...", - prefix=app.app_name, - ) - # Start adb's logcat in a way that lets us stream the logs - log_popen = adb.logcat(pid=pid) - - # Stream the app logs. - self._stream_app_logs( - app, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - # Check for the PID in quiet mode so logs aren't corrupted. - stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), - log_stream=True, - ) - else: - self.console.error("Unable to find PID for app", prefix=app.app_name) - self.console.error("Logs for launch attempt follow...") - self.console.error("=" * 75) - - # Show the log from the start time of the app - adb.logcat_tail(since=device_start_time) + forward_ports = forward_ports or [] + reverse_ports = reverse_ports or [] + + # Forward/Reverse requested ports + with self.forward_ports(adb, forward_ports, reverse_ports): + # To start the app, we launch `org.beeware.android.MainActivity`. + with self.console.wait_bar(f"Launching {label}..."): + # capture the earliest time for device logging in case PID not found + device_start_time = adb.datetime() + + adb.start_app( + package, "org.beeware.android.MainActivity", passthrough + ) + + # Try to get the PID for 5 seconds. + pid = None + fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) + while not pid and datetime.datetime.now() < fail_time: + # Try to get the PID; run in quiet mode because we may + # need to do this a lot in the next 5 seconds. + pid = adb.pidof(package, quiet=2) + if not pid: + time.sleep(0.01) + + if pid: + self.console.info( + "Following device log output (type CTRL-C to stop log)...", + prefix=app.app_name, + ) + # Start adb's logcat in a way that lets us stream the logs + log_popen = adb.logcat(pid=pid) + + # Stream the app logs. + self._stream_app_logs( + app, + popen=log_popen, + clean_filter=android_log_clean_filter, + clean_output=False, + # Check for the PID in quiet mode so logs aren't corrupted. + stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), + log_stream=True, + ) + else: + self.console.error( + "Unable to find PID for app", prefix=app.app_name + ) + self.console.error("Logs for launch attempt follow...") + self.console.error("=" * 75) + + # Show the log from the start time of the app + adb.logcat_tail(since=device_start_time) + + raise BriefcaseCommandError( + f"Problem starting app {app.app_name!r}" + ) - raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() + @contextlib.contextmanager + def forward_ports( + self, adb: ADB, forward_ports: list[int], reverse_ports: list[int] + ): + """Establish a port forwarding/reversion. + + :param adb: The ADB wrapper for the device + :param forward_ports: Ports to forward via ADB + :param reverse_ports: Ports to reverse via ADB + """ + for port in forward_ports: + adb.forward(port, port) + for port in reverse_ports: + adb.reverse(port, port) + + yield + + for port in forward_ports: + adb.forward_remove(port) + for port in reverse_ports: + adb.reverse_remove(port) + class GradlePackageCommand(GradleMixin, PackageCommand): description = "Create an Android App Bundle and APK in release mode." diff --git a/tests/integrations/android_sdk/ADB/test_forward_reverse.py b/tests/integrations/android_sdk/ADB/test_forward_reverse.py new file mode 100644 index 000000000..3d05cb54c --- /dev/null +++ b/tests/integrations/android_sdk/ADB/test_forward_reverse.py @@ -0,0 +1,117 @@ +import subprocess + +import pytest + +from briefcase.exceptions import BriefcaseCommandError + + +def test_forward(mock_tools, adb): + """An port forwarding.""" + # Invoke forward + adb.forward(5555, 6666) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "forward", + "tcp:5555", + "tcp:6666", + ], + ) + + +def test_forward_failure(adb, mock_tools): + """If port forwarding fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.forward(5555, 6666) + + +def test_forward_remove(mock_tools, adb): + """An port forwarding removing.""" + # Invoke forward remove + adb.forward_remove(5555) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "forward", + "--remove", + "tcp:5555", + ], + ) + + +def test_forward_remove_failure(adb, mock_tools): + """If port forwarding removing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.forward_remove(5555) + + +def test_reverse(mock_tools, adb): + """An port reversing.""" + # Invoke reverse + adb.reverse(5555, 6666) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "reverse", + "tcp:5555", + "tcp:6666", + ], + ) + + +def test_reverse_failure(adb, mock_tools): + """If port reversing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.reverse(5555, 6666) + + +def test_reverse_remove(mock_tools, adb): + """An port reversing removing.""" + # Invoke reverse remove + adb.reverse_remove(5555) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "reverse", + "--remove", + "tcp:5555", + ], + ) + + +def test_reverse_remove_failure(adb, mock_tools): + """If port reversing removing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.reverse_remove(5555) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 2f6fb6485..76842227b 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -93,6 +93,8 @@ def test_device_option(run_command): "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": False, + "forward_ports": [], + "reverse_ports": [], } assert overrides == {} @@ -116,6 +118,8 @@ def test_extra_emulator_args_option(run_command): "passthrough": [], "extra_emulator_args": ["-no-window", "-no-audio"], "shutdown_on_exit": False, + "forward_ports": [], + "reverse_ports": [], } assert overrides == {} @@ -137,6 +141,58 @@ def test_shutdown_on_exit_option(run_command): "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": True, + "forward_ports": [], + "reverse_ports": [], + } + assert overrides == {} + + +def test_forward_ports_option(run_command): + """The -d option can be parsed.""" + options, overrides = run_command.parse_options( + ["--forward-port", "80", "--forward-port", "81"] + ) + + assert options == { + "device_or_avd": None, + "appname": None, + "update": False, + "update_requirements": False, + "update_resources": False, + "update_support": False, + "update_stub": False, + "no_update": False, + "test_mode": False, + "passthrough": [], + "extra_emulator_args": None, + "shutdown_on_exit": True, + "forward_ports": [80, 81], + "reverse_ports": [], + } + assert overrides == {} + + +def test_reverse_ports_option(run_command): + """The -d option can be parsed.""" + options, overrides = run_command.parse_options( + ["--reverse-port", "78", "--reverse-port", "79"] + ) + + assert options == { + "device_or_avd": None, + "appname": None, + "update": False, + "update_requirements": False, + "update_resources": False, + "update_support": False, + "update_stub": False, + "no_update": False, + "test_mode": False, + "passthrough": [], + "extra_emulator_args": None, + "shutdown_on_exit": True, + "forward_ports": [], + "reverse_ports": [78, 79], } assert overrides == {} @@ -306,6 +362,94 @@ def mock_stream_output(app, stop_func, **kwargs): run_command.tools.mock_adb.kill.assert_not_called() +def test_run_forward_reverse_ports(run_command, first_app_config): + """An app can be run with passthrough args.""" + # Set up device selection to return a running physical device. + run_command.tools.android_sdk.select_target_device = mock.MagicMock( + return_value=("exampleDevice", "ExampleDevice", None) + ) + + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.mock_adb.logcat.return_value = log_popen + + # To satisfy coverage, the stop function must be invoked at least once + # when invoking stream_output. + def mock_stream_output(app, stop_func, **kwargs): + stop_func() + + run_command._stream_app_logs.side_effect = mock_stream_output + + # Set up app config to have a `-` in the `bundle`, to ensure it gets + # normalized into a `_` via `package_name`. + first_app_config.bundle = "com.ex-ample" + + # Invoke run_app with args. + run_command.run_app( + first_app_config, + device_or_avd="exampleDevice", + passthrough=[], + forward_ports=[80, 81], + reverse_ports=[78, 79], + ) + + # select_target_device was invoked with a specific device + run_command.tools.android_sdk.select_target_device.assert_called_once_with( + "exampleDevice" + ) + + # The ADB wrapper is created + run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") + + # The adb wrapper is invoked with the expected arguments + run_command.tools.mock_adb.install_apk.assert_called_once_with( + run_command.binary_path(first_app_config) + ) + run_command.tools.mock_adb.force_stop_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + ) + + assert run_command.tools.mock_adb.forward.call_count == 2 + run_command.tools.mock_adb.forward.assert_any_call(80, 80) + run_command.tools.mock_adb.forward.assert_any_call(81, 81) + + assert run_command.tools.mock_adb.forward_remove.call_count == 2 + run_command.tools.mock_adb.forward_remove.assert_any_call(80) + run_command.tools.mock_adb.forward_remove.assert_any_call(81) + + assert run_command.tools.mock_adb.reverse.call_count == 2 + run_command.tools.mock_adb.reverse.assert_any_call(78, 78) + run_command.tools.mock_adb.reverse.assert_any_call(79, 79) + + assert run_command.tools.mock_adb.reverse_remove.call_count == 2 + run_command.tools.mock_adb.reverse_remove.assert_any_call(78) + run_command.tools.mock_adb.reverse_remove.assert_any_call(79) + + run_command.tools.mock_adb.start_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + "org.beeware.android.MainActivity", + [], + ) + + run_command.tools.mock_adb.pidof.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + quiet=2, + ) + run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") + + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + clean_filter=android_log_clean_filter, + clean_output=False, + stop_func=mock.ANY, + log_stream=True, + ) + + # The emulator was not killed at the end of the test + run_command.tools.mock_adb.kill.assert_not_called() + + def test_run_slow_start(run_command, first_app_config, monkeypatch): """If the app is slow to start, multiple calls to pidof will be made.""" run_command.tools.android_sdk.select_target_device = mock.MagicMock( From ef3a4600398f59462d16d67b9616e297f1c02ba4 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:14:47 +0200 Subject: [PATCH 2/7] add documentation --- docs/reference/platforms/android/gradle.rst | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/reference/platforms/android/gradle.rst b/docs/reference/platforms/android/gradle.rst index 43d63b84c..ca97030f9 100644 --- a/docs/reference/platforms/android/gradle.rst +++ b/docs/reference/platforms/android/gradle.rst @@ -159,7 +159,7 @@ You may specify multiple ``--Xemulator`` arguments; each one specifies a single argument to pass to the emulator, in the order they are specified. ``--shutdown-on-exit`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~ Instruct Briefcase to shut down the emulator when the run finishes. This is especially useful if you are running in headless mode, as the emulator will @@ -167,6 +167,27 @@ continue to run in the background, but there will be no visual manifestation that it is running. It may also be useful as a cleanup mechanism when running in a CI configuration. +``--forward-port=`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Forward a port via ADB from the host to the Android device. This is useful when +a network service is running on the Android app that you want to connect to from +the host. + +You may specify multiple ``--forward-port`` arguments; each one specifies a +single port. + +``--reverse-port=`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Reverse a port via ADB from the Android device to the host. This is useful when +a network service is running on the host that you want to connect to from the +Android app. + +You may specify multiple ``--reverse-port`` arguments; each one specifies a +single port. + + Application configuration ========================= From 5153a3aba37d4103f9c938cefaacc959d66a22ac Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:25:59 +0200 Subject: [PATCH 3/7] added changelog --- changes/2369.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2369.feature.rst diff --git a/changes/2369.feature.rst b/changes/2369.feature.rst new file mode 100644 index 000000000..43149ac6f --- /dev/null +++ b/changes/2369.feature.rst @@ -0,0 +1 @@ +Add ``--forward-port`` and ``--reverse-port`` via ADB when running an Android app From 8c3bec9218f16a009da42b15fcbd35ffea1ecf8d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:37:36 +0200 Subject: [PATCH 4/7] fixed unit tests --- tests/platforms/android/gradle/test_run.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 76842227b..2613005b4 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -93,8 +93,8 @@ def test_device_option(run_command): "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": False, - "forward_ports": [], - "reverse_ports": [], + "forward_ports": None, + "reverse_ports": None, } assert overrides == {} @@ -118,8 +118,8 @@ def test_extra_emulator_args_option(run_command): "passthrough": [], "extra_emulator_args": ["-no-window", "-no-audio"], "shutdown_on_exit": False, - "forward_ports": [], - "reverse_ports": [], + "forward_ports": None, + "reverse_ports": None, } assert overrides == {} @@ -141,8 +141,8 @@ def test_shutdown_on_exit_option(run_command): "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": True, - "forward_ports": [], - "reverse_ports": [], + "forward_ports": None, + "reverse_ports": None, } assert overrides == {} @@ -165,9 +165,9 @@ def test_forward_ports_option(run_command): "test_mode": False, "passthrough": [], "extra_emulator_args": None, - "shutdown_on_exit": True, + "shutdown_on_exit": False, "forward_ports": [80, 81], - "reverse_ports": [], + "reverse_ports": None, } assert overrides == {} @@ -190,8 +190,8 @@ def test_reverse_ports_option(run_command): "test_mode": False, "passthrough": [], "extra_emulator_args": None, - "shutdown_on_exit": True, - "forward_ports": [], + "shutdown_on_exit": False, + "forward_ports": None, "reverse_ports": [78, 79], } assert overrides == {} From cd855ef8f312cb457544653c007f540364c8cadf Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:54:33 +0200 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Malcolm Smith --- src/briefcase/platforms/android/gradle.py | 2 -- .../android_sdk/ADB/test_forward_reverse.py | 12 ++++++------ tests/platforms/android/gradle/test_run.py | 6 +++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 40844c1a2..cc34b8fbd 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -365,7 +365,6 @@ def add_options(self, parser): dest="forward_ports", type=int, help="Forward the specified port from host to device.", - required=False, ) parser.add_argument( "--reverse-port", @@ -373,7 +372,6 @@ def add_options(self, parser): dest="reverse_ports", type=int, help="Reverse the specified port from device to host.", - required=False, ) def run_app( diff --git a/tests/integrations/android_sdk/ADB/test_forward_reverse.py b/tests/integrations/android_sdk/ADB/test_forward_reverse.py index 3d05cb54c..c2418a68b 100644 --- a/tests/integrations/android_sdk/ADB/test_forward_reverse.py +++ b/tests/integrations/android_sdk/ADB/test_forward_reverse.py @@ -6,7 +6,7 @@ def test_forward(mock_tools, adb): - """An port forwarding.""" + """A port forwarding""" # Invoke forward adb.forward(5555, 6666) @@ -34,7 +34,7 @@ def test_forward_failure(adb, mock_tools): def test_forward_remove(mock_tools, adb): - """An port forwarding removing.""" + """A port forwarding removal.""" # Invoke forward remove adb.forward_remove(5555) @@ -52,7 +52,7 @@ def test_forward_remove(mock_tools, adb): def test_forward_remove_failure(adb, mock_tools): - """If port forwarding removing fails, the error is caught.""" + """If port forwarding removal fails, the error is caught.""" # Mock out the run command on an adb instance mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( returncode=1, cmd="" @@ -62,7 +62,7 @@ def test_forward_remove_failure(adb, mock_tools): def test_reverse(mock_tools, adb): - """An port reversing.""" + """A port reversing.""" # Invoke reverse adb.reverse(5555, 6666) @@ -90,7 +90,7 @@ def test_reverse_failure(adb, mock_tools): def test_reverse_remove(mock_tools, adb): - """An port reversing removing.""" + """A port reversing removal.""" # Invoke reverse remove adb.reverse_remove(5555) @@ -108,7 +108,7 @@ def test_reverse_remove(mock_tools, adb): def test_reverse_remove_failure(adb, mock_tools): - """If port reversing removing fails, the error is caught.""" + """If port reversing removal fails, the error is caught.""" # Mock out the run command on an adb instance mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( returncode=1, cmd="" diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 2613005b4..5475a2488 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -148,7 +148,7 @@ def test_shutdown_on_exit_option(run_command): def test_forward_ports_option(run_command): - """The -d option can be parsed.""" + """The --forward-port option can be parsed.""" options, overrides = run_command.parse_options( ["--forward-port", "80", "--forward-port", "81"] ) @@ -173,7 +173,7 @@ def test_forward_ports_option(run_command): def test_reverse_ports_option(run_command): - """The -d option can be parsed.""" + """The --reverse-port option can be parsed.""" options, overrides = run_command.parse_options( ["--reverse-port", "78", "--reverse-port", "79"] ) @@ -363,7 +363,7 @@ def mock_stream_output(app, stop_func, **kwargs): def test_run_forward_reverse_ports(run_command, first_app_config): - """An app can be run with passthrough args.""" + """An app can be run with port forwarding and reversing.""" # Set up device selection to return a running physical device. run_command.tools.android_sdk.select_target_device = mock.MagicMock( return_value=("exampleDevice", "ExampleDevice", None) From c2979ef042e1b3658a34936419adac3058b64909 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:13:18 +0200 Subject: [PATCH 6/7] simplified test and added tests that forward/reverse/remove are not called --- tests/platforms/android/gradle/test_run.py | 86 ++++++---------------- 1 file changed, 22 insertions(+), 64 deletions(-) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 5475a2488..0b7a4a9ce 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -267,12 +267,18 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", ) + assert run_command.tools.mock_adb.forward.call_count == 0 + assert run_command.tools.mock_adb.reverse.call_count == 0 + run_command.tools.mock_adb.start_app.assert_called_once_with( f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], ) + assert run_command.tools.mock_adb.forward_remove.call_count == 0 + assert run_command.tools.mock_adb.reverse_remove.call_count == 0 + run_command.tools.mock_adb.pidof.assert_called_once_with( f"{first_app_config.package_name}.{first_app_config.module_name}", quiet=2, @@ -369,61 +375,22 @@ def test_run_forward_reverse_ports(run_command, first_app_config): return_value=("exampleDevice", "ExampleDevice", None) ) - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.mock_adb.logcat.return_value = log_popen - - # To satisfy coverage, the stop function must be invoked at least once - # when invoking stream_output. - def mock_stream_output(app, stop_func, **kwargs): - stop_func() - - run_command._stream_app_logs.side_effect = mock_stream_output - - # Set up app config to have a `-` in the `bundle`, to ensure it gets - # normalized into a `_` via `package_name`. - first_app_config.bundle = "com.ex-ample" - # Invoke run_app with args. run_command.run_app( first_app_config, - device_or_avd="exampleDevice", passthrough=[], forward_ports=[80, 81], reverse_ports=[78, 79], ) - # select_target_device was invoked with a specific device - run_command.tools.android_sdk.select_target_device.assert_called_once_with( - "exampleDevice" - ) - - # The ADB wrapper is created - run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") - - # The adb wrapper is invoked with the expected arguments - run_command.tools.mock_adb.install_apk.assert_called_once_with( - run_command.binary_path(first_app_config) - ) - run_command.tools.mock_adb.force_stop_app.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - ) - - assert run_command.tools.mock_adb.forward.call_count == 2 - run_command.tools.mock_adb.forward.assert_any_call(80, 80) - run_command.tools.mock_adb.forward.assert_any_call(81, 81) - - assert run_command.tools.mock_adb.forward_remove.call_count == 2 - run_command.tools.mock_adb.forward_remove.assert_any_call(80) - run_command.tools.mock_adb.forward_remove.assert_any_call(81) - - assert run_command.tools.mock_adb.reverse.call_count == 2 - run_command.tools.mock_adb.reverse.assert_any_call(78, 78) - run_command.tools.mock_adb.reverse.assert_any_call(79, 79) - - assert run_command.tools.mock_adb.reverse_remove.call_count == 2 - run_command.tools.mock_adb.reverse_remove.assert_any_call(78) - run_command.tools.mock_adb.reverse_remove.assert_any_call(79) + assert run_command.tools.mock_adb.forward.mock_calls == [ + mock.call(80, 80), + mock.call(81, 81), + ] + assert run_command.tools.mock_adb.reverse.mock_calls == [ + mock.call(78, 78), + mock.call(79, 79), + ] run_command.tools.mock_adb.start_app.assert_called_once_with( f"{first_app_config.package_name}.{first_app_config.module_name}", @@ -431,23 +398,14 @@ def mock_stream_output(app, stop_func, **kwargs): [], ) - run_command.tools.mock_adb.pidof.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - quiet=2, - ) - run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") - - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - stop_func=mock.ANY, - log_stream=True, - ) - - # The emulator was not killed at the end of the test - run_command.tools.mock_adb.kill.assert_not_called() + assert run_command.tools.mock_adb.forward_remove.mock_calls == [ + mock.call(80), + mock.call(81), + ] + assert run_command.tools.mock_adb.reverse_remove.mock_calls == [ + mock.call(78), + mock.call(79), + ] def test_run_slow_start(run_command, first_app_config, monkeypatch): From d47e7ee4890285614be0eb6f950a0d0734a0c91d Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Thu, 10 Jul 2025 15:22:22 +0100 Subject: [PATCH 7/7] Replace `assert call_count == 0` with `assert_not_called` --- tests/platforms/android/gradle/test_run.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 0b7a4a9ce..c6aacea55 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -267,8 +267,8 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", ) - assert run_command.tools.mock_adb.forward.call_count == 0 - assert run_command.tools.mock_adb.reverse.call_count == 0 + run_command.tools.mock_adb.forward.assert_not_called() + run_command.tools.mock_adb.reverse.assert_not_called() run_command.tools.mock_adb.start_app.assert_called_once_with( f"{first_app_config.package_name}.{first_app_config.module_name}", @@ -276,8 +276,8 @@ def mock_stream_output(app, stop_func, **kwargs): [], ) - assert run_command.tools.mock_adb.forward_remove.call_count == 0 - assert run_command.tools.mock_adb.reverse_remove.call_count == 0 + run_command.tools.mock_adb.forward_remove.assert_not_called() + run_command.tools.mock_adb.reverse_remove.assert_not_called() run_command.tools.mock_adb.pidof.assert_called_once_with( f"{first_app_config.package_name}.{first_app_config.module_name}",