diff --git a/android_env/components/config_classes.py b/android_env/components/config_classes.py index 528974f..a1792dc 100644 --- a/android_env/components/config_classes.py +++ b/android_env/components/config_classes.py @@ -100,10 +100,6 @@ class GPUMode(enum.Enum): class EmulatorLauncherConfig: """Config class for EmulatorLauncher.""" - # NOTE: If `adb_port`, `emulator_console_port` and `grpc_port` are defined - # (i.e. not all equal to 0), it is assumed that the emulator they point to - # exists already and EmulatorLauncher will be skipped. - # Filesystem path to the `emulator` binary. emulator_path: str = '~/Android/Sdk/emulator/emulator' # Filesystem path to the Android SDK root. @@ -138,6 +134,11 @@ class EmulatorLauncherConfig: emulator_console_port: int = 0 # Port for gRPC communication with the emulator. grpc_port: int = 0 + # Whether to connect to an existing emulator. + # If True, we connect to the emulator at the specified ports and do not + # launch. + # If False, we launch a new emulator (ports must be pre-allocated). + connect_to_existing: bool = False @dataclasses.dataclass diff --git a/android_env/components/simulators/emulator/emulator_simulator.py b/android_env/components/simulators/emulator/emulator_simulator.py index 4e7af5c..93fee39 100644 --- a/android_env/components/simulators/emulator/emulator_simulator.py +++ b/android_env/components/simulators/emulator/emulator_simulator.py @@ -34,7 +34,6 @@ import grpc import immutabledict import numpy as np -import portpicker from android_env.proto import emulator_controller_pb2 from android_env.proto import emulator_controller_pb2_grpc @@ -54,50 +53,6 @@ }) -def _is_existing_emulator_provided( - launcher_config: config_classes.EmulatorLauncherConfig, -) -> bool: - """Returns true if all necessary args were provided.""" - - return bool( - launcher_config.adb_port - and launcher_config.emulator_console_port - and launcher_config.grpc_port - ) - - -def _pick_adb_port() -> int: - """Tries to pick a port in the recommended range 5555-5585. - - If no such port can be found, will return a random unused port. More info: - https://developer.android.com/studio/command-line/adb#howadbworks. - - Returns: - port: an available port for adb. - """ - - for p in range(5555, 5587, 2): - if portpicker.is_port_free(p): - return p - return portpicker.pick_unused_port() - - -def _pick_emulator_grpc_port() -> int: - """Tries to pick the recommended port for grpc. - - If no such port can be found, will return a random unused port. More info: - https://android.googlesource.com/platform/external/qemu/+/emu-master-dev/android/android-grpc/docs/. - - Returns: - port: an available port for emulator grpc. - """ - - if portpicker.is_port_free(8554): - return 8554 - else: - return portpicker.pick_unused_port() - - class EmulatorBootError(errors.SimulatorError): """Raised when an emulator failed to boot.""" @@ -159,17 +114,23 @@ def __init__( # If adb_port, console_port and grpc_port are all already provided, # we assume the emulator already exists and there's no need to launch. - if _is_existing_emulator_provided(self._config.emulator_launcher): - self._existing_emulator_provided = True - logging.info('Connecting to existing emulator "%r"', - self.adb_device_name()) - else: - self._existing_emulator_provided = False - self._config.emulator_launcher.adb_port = _pick_adb_port() - self._config.emulator_launcher.emulator_console_port = ( - portpicker.pick_unused_port() + if self._config.emulator_launcher.connect_to_existing: + logging.info( + 'Connecting to existing emulator "%r"', self.adb_device_name() ) - self._config.emulator_launcher.grpc_port = _pick_emulator_grpc_port() + else: + launcher_config = self._config.emulator_launcher + if ( + launcher_config.adb_port <= 0 + or launcher_config.emulator_console_port <= 0 + or launcher_config.grpc_port <= 0 + ): + raise ValueError( + 'Ports must be allocated and set in config before instantiating ' + f'EmulatorSimulator. Got: ADB={launcher_config.adb_port}, ' + f'Console={launcher_config.emulator_console_port}, ' + f'gRPC={launcher_config.grpc_port}' + ) self._channel = None self._emulator_stub: emulator_controller_pb2_grpc.EmulatorControllerStub | None = ( @@ -202,7 +163,7 @@ def __init__( ) # If necessary, create EmulatorLauncher. - if self._existing_emulator_provided: + if self._config.emulator_launcher.connect_to_existing: self._logfile_path = self._config.logfile_path or None self._launcher = None else: diff --git a/android_env/components/simulators/emulator/emulator_simulator_test.py b/android_env/components/simulators/emulator/emulator_simulator_test.py index 708a274..257bcd8 100644 --- a/android_env/components/simulators/emulator/emulator_simulator_test.py +++ b/android_env/components/simulators/emulator/emulator_simulator_test.py @@ -30,13 +30,28 @@ from android_env.proto import state_pb2 import grpc from PIL import Image -import portpicker from android_env.proto import emulator_controller_pb2 from android_env.proto import emulator_controller_pb2_grpc from android_env.proto import snapshot_service_pb2 +def _launcher_config( + adb_port: int = 5555, + emulator_console_port: int = 5554, + grpc_port: int = 1234, + connect_to_existing: bool = False, + **kwargs, +) -> config_classes.EmulatorLauncherConfig: + return config_classes.EmulatorLauncherConfig( + adb_port=adb_port, + emulator_console_port=emulator_console_port, + grpc_port=grpc_port, + connect_to_existing=connect_to_existing, + **kwargs, + ) + + class EmulatorSimulatorTest(parameterized.TestCase): def setUp(self): @@ -95,7 +110,7 @@ def setUp(self): def test_adb_device_name_not_empty(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -111,7 +126,7 @@ def test_logfile_path(self): config = config_classes.EmulatorConfig( logfile_path='fake/logfile/path', - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -130,38 +145,6 @@ def test_logfile_path(self): mock_open.assert_called_once_with('fake/logfile/path', 'rb') self.assertEqual(logs, 'fake_logs') - @mock.patch.object(portpicker, 'is_port_free', return_value=True) - def test_grpc_port(self, unused_mock_portpicker): - - launcher_config = config_classes.EmulatorLauncherConfig( - tmp_dir=self.create_tempdir().full_path - ) - config = config_classes.EmulatorConfig( - emulator_launcher=launcher_config, - adb_controller=config_classes.AdbControllerConfig( - adb_path='/my/adb', - adb_server_port=5037, - ), - ) - simulator = emulator_simulator.EmulatorSimulator(config) - self.assertEqual(launcher_config.grpc_port, 8554) - - @mock.patch.object(portpicker, 'is_port_free', return_value=False) - def test_grpc_port_unavailable(self, unused_mock_portpicker): - - launcher_config = config_classes.EmulatorLauncherConfig( - tmp_dir=self.create_tempdir().full_path - ) - config = config_classes.EmulatorConfig( - emulator_launcher=launcher_config, - adb_controller=config_classes.AdbControllerConfig( - adb_path='/my/adb', - adb_server_port=5037, - ), - ) - simulator = emulator_simulator.EmulatorSimulator(config) - self.assertNotEqual(launcher_config.grpc_port, 8554) - def test_launch_operation_order(self): """Makes sure that adb_controller is started before Emulator is launched.""" @@ -174,7 +157,7 @@ def test_launch_operation_order(self): lambda: call_order.append('launch_emulator_process') ) config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -193,7 +176,7 @@ def test_launch_operation_order(self): def test_close(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -216,7 +199,7 @@ def test_value_error_if_launch_attempt_params_incorrect(self): ValueError, emulator_simulator.EmulatorSimulator, config=config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -228,9 +211,35 @@ def test_value_error_if_launch_attempt_params_incorrect(self): ), ) + @parameterized.parameters( + (0, 5554, 1234), + (5555, 0, 1234), + (5555, 5554, 0), + (-1, 5554, 1234), + (5555, -1, 1234), + (5555, 5554, -1), + ) + def test_value_error_if_ports_not_positive( + self, adb_port, console_port, grpc_port + ): + config = config_classes.EmulatorConfig( + emulator_launcher=_launcher_config( + adb_port=adb_port, + emulator_console_port=console_port, + grpc_port=grpc_port, + tmp_dir=self.create_tempdir().full_path, + ), + adb_controller=config_classes.AdbControllerConfig( + adb_path='/my/adb', + adb_server_port=5037, + ), + ) + with self.assertRaisesRegex(ValueError, 'Ports must be allocated and set'): + emulator_simulator.EmulatorSimulator(config) + def test_launch_attempt_reboot(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -256,7 +265,7 @@ def test_launch_attempt_reboot(self): def test_launch_attempt_reinstall_after_zero_attempts(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -283,7 +292,7 @@ def test_launch_attempt_reinstall_after_zero_attempts(self): def test_launch_attempt_reinstall(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -315,7 +324,7 @@ def test_launch_attempt_reinstall(self): def test_get_screenshot(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -343,7 +352,7 @@ def test_get_screenshot(self): def test_get_screenshot_reconnects_on_grpc_error(self): config = config_classes.EmulatorConfig( interaction_rate_sec=0.0, - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -389,7 +398,7 @@ def test_get_screenshot_reconnects_on_grpc_error(self): def test_load_state(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -443,7 +452,7 @@ def test_load_state(self): def test_save_state(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -485,7 +494,7 @@ def test_save_state(self): def test_send_touch(self): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -564,7 +573,7 @@ def test_send_touch(self): ) def test_send_key(self, os_name, expected_code_type): config = config_classes.EmulatorConfig( - emulator_launcher=config_classes.EmulatorLauncherConfig( + emulator_launcher=_launcher_config( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), adb_controller=config_classes.AdbControllerConfig( @@ -635,6 +644,30 @@ def test_send_key(self, os_name, expected_code_type): expected_calls, simulator._emulator_stub.sendKey.call_args_list ) + def test_connect_to_existing(self): + config = config_classes.EmulatorConfig( + emulator_launcher=_launcher_config( + adb_port=5555, + emulator_console_port=5554, + grpc_port=1234, + connect_to_existing=True, + tmp_dir=self.create_tempdir().full_path, + ), + adb_controller=config_classes.AdbControllerConfig( + adb_path='/my/adb', + adb_server_port=5037, + ), + ) + simulator = emulator_simulator.EmulatorSimulator(config) + self.addCleanup(simulator.close) + + # Launch should not try to launch the process, but should connect. + simulator.launch() + + self.assertIsNone(simulator._launcher) + # It should still establish channel. + self.assertIsNotNone(simulator._emulator_stub) + if __name__ == '__main__': absltest.main() diff --git a/android_env/components/simulators/emulator/tcp_ports.py b/android_env/components/simulators/emulator/tcp_ports.py new file mode 100644 index 0000000..1352595 --- /dev/null +++ b/android_env/components/simulators/emulator/tcp_ports.py @@ -0,0 +1,59 @@ +# coding=utf-8 +# Copyright 2026 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for allocating emulator ports.""" + +from absl import logging +from android_env.components import config_classes +import portpicker + + +def _pick_adb_port() -> int: + """Finds a free port for ADB in the standard range.""" + for p in range(5555, 5587, 2): + if portpicker.is_port_free(p): + return p + return portpicker.pick_unused_port() + + +def _pick_emulator_grpc_port() -> int: + """Finds a free port for Emulator gRPC.""" + if portpicker.is_port_free(8554): + return 8554 + else: + return portpicker.pick_unused_port() + + +def pick_emulator_ports(config: config_classes.EmulatorLauncherConfig) -> None: + """Allocates ports for the emulator if they are not already set.""" + allocated_any = False + if config.adb_port <= 0: + config.adb_port = _pick_adb_port() + allocated_any = True + if config.emulator_console_port <= 0: + config.emulator_console_port = portpicker.pick_unused_port() + allocated_any = True + if config.grpc_port <= 0: + config.grpc_port = _pick_emulator_grpc_port() + allocated_any = True + + if allocated_any: + config.connect_to_existing = False + logging.info( + 'Allocated ports for emulator: ADB=%d, Console=%d, gRPC=%d', + config.adb_port, + config.emulator_console_port, + config.grpc_port, + ) diff --git a/android_env/components/simulators/emulator/tcp_ports_test.py b/android_env/components/simulators/emulator/tcp_ports_test.py new file mode 100644 index 0000000..2d296d0 --- /dev/null +++ b/android_env/components/simulators/emulator/tcp_ports_test.py @@ -0,0 +1,187 @@ +# coding=utf-8 +# Copyright 2026 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for TCP ports allocation utilities.""" + +from unittest import mock + +from absl.testing import absltest +from android_env.components import config_classes +from android_env.components.simulators.emulator import tcp_ports +import portpicker + + +class TcpPortsTest(absltest.TestCase): + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_all_unset( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5555) + self.assertEqual(launcher_config.emulator_console_port, 9999) + self.assertEqual(launcher_config.grpc_port, 8554) + + def test_pick_ports_already_set(self): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=1111, + emulator_console_port=2222, + grpc_port=3333, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 1111) + self.assertEqual(launcher_config.emulator_console_port, 2222) + self.assertEqual(launcher_config.grpc_port, 3333) + self.assertTrue(launcher_config.connect_to_existing) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_partial_resets_connect_to_existing( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=1111, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 1111) + self.assertEqual(launcher_config.emulator_console_port, 9999) + self.assertEqual(launcher_config.grpc_port, 8554) + self.assertFalse(launcher_config.connect_to_existing) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + def test_grpc_port(self, unused_mock_portpicker): + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.grpc_port, 8554) + + @mock.patch.object(portpicker, 'is_port_free', return_value=False) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1234) + def test_grpc_port_unavailable( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.grpc_port, 1234) + + @mock.patch.object(portpicker, 'is_port_free') + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1234) + def test_adb_port_selection(self, unused_mock_pick_unused, mock_is_port_free): + # 5555 is busy, 5557 is free + mock_is_port_free.side_effect = lambda p: p == 5557 + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5557) + + @mock.patch.object(portpicker, 'is_port_free', return_value=False) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1234) + def test_adb_port_all_busy( + self, unused_mock_pick_unused, unused_mock_is_port_free + ): + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 1234) + + @mock.patch.object(portpicker, 'is_port_free') + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1234) + def test_adb_port_selection_skips_even_ports( + self, unused_mock_pick_unused, mock_is_port_free + ): + # 5555 is busy, 5556 is free, 5557 is free. + # It should skip 5556 and pick 5557. + mock_is_port_free.side_effect = lambda p: p in (5556, 5557) + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5557) + + @mock.patch.object(portpicker, 'is_port_free') + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1234) + def test_adb_port_last_in_range( + self, unused_mock_pick_unused, mock_is_port_free + ): + # Only the last port in the range (5585) is free. + mock_is_port_free.side_effect = lambda p: p == 5585 + launcher_config = config_classes.EmulatorLauncherConfig() + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5585) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_negative( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=-1, + emulator_console_port=-1, + grpc_port=-1, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5555) + self.assertEqual(launcher_config.emulator_console_port, 9999) + self.assertEqual(launcher_config.grpc_port, 8554) + self.assertFalse(launcher_config.connect_to_existing) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_only_adb_allocated_resets_connect_to_existing( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=0, + emulator_console_port=2222, + grpc_port=3333, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.adb_port, 5555) + self.assertFalse(launcher_config.connect_to_existing) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_only_console_allocated_resets_connect_to_existing( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=1111, + emulator_console_port=0, + grpc_port=3333, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.emulator_console_port, 9999) + self.assertFalse(launcher_config.connect_to_existing) + + @mock.patch.object(portpicker, 'is_port_free', return_value=True) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=9999) + def test_pick_ports_only_grpc_allocated_resets_connect_to_existing( + self, unused_mock_pick_unused, unused_mock_is_free + ): + launcher_config = config_classes.EmulatorLauncherConfig( + adb_port=1111, + emulator_console_port=2222, + grpc_port=0, + connect_to_existing=True, + ) + tcp_ports.pick_emulator_ports(launcher_config) + self.assertEqual(launcher_config.grpc_port, 8554) + self.assertFalse(launcher_config.connect_to_existing) + + +if __name__ == '__main__': + absltest.main() diff --git a/android_env/loader.py b/android_env/loader.py index 6a96b7d..aac854d 100644 --- a/android_env/loader.py +++ b/android_env/loader.py @@ -24,6 +24,7 @@ from android_env.components import device_settings as device_settings_lib from android_env.components import task_manager as task_manager_lib from android_env.components.simulators.emulator import emulator_simulator +from android_env.components.simulators.emulator import tcp_ports from android_env.components.simulators.fake import fake_simulator from android_env.proto import task_pb2 from google.protobuf import text_format @@ -52,6 +53,7 @@ def load(config: config_classes.AndroidEnvConfig) -> environment.AndroidEnv: match config.simulator: case config_classes.EmulatorConfig(): _process_emulator_launcher_config(config.simulator) + tcp_ports.pick_emulator_ports(config.simulator.emulator_launcher) simulator = emulator_simulator.EmulatorSimulator(config=config.simulator) case config_classes.FakeSimulatorConfig(): simulator = fake_simulator.FakeSimulator(config=config.simulator) diff --git a/android_env/loader_test.py b/android_env/loader_test.py index 4605b9f..6a56c9f 100644 --- a/android_env/loader_test.py +++ b/android_env/loader_test.py @@ -13,10 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. - - import builtins import os +from typing import cast from unittest import mock from absl.testing import absltest @@ -27,6 +26,7 @@ from android_env.components import device_settings as device_settings_lib from android_env.components import task_manager as task_manager_lib from android_env.components.simulators.emulator import emulator_simulator +from android_env.components.simulators.emulator import tcp_ports from android_env.components.simulators.fake import fake_simulator from android_env.proto import task_pb2 @@ -37,10 +37,12 @@ class LoaderTest(absltest.TestCase): @mock.patch.object(emulator_simulator, 'EmulatorSimulator', autospec=True) @mock.patch.object(device_settings_lib, 'DeviceSettings', autospec=True) @mock.patch.object(coordinator_lib, 'Coordinator', autospec=True) + @mock.patch.object(tcp_ports, 'pick_emulator_ports', autospec=True) @mock.patch.object(builtins, 'open', autospec=True) def test_load_emulator( self, mock_open, + mock_pick_ports, mock_coordinator, mock_device_settings, mock_simulator_class, @@ -71,6 +73,8 @@ def test_load_emulator( # Assert. self.assertIsInstance(env, env_interface.AndroidEnvInterface) + emu_config = cast(config_classes.EmulatorConfig, config.simulator) + mock_pick_ports.assert_called_once_with(emu_config.emulator_launcher) mock_simulator_class.assert_called_with( config=config_classes.EmulatorConfig( emulator_launcher=config_classes.EmulatorLauncherConfig( diff --git a/android_env/wrappers/a11y_grpc_wrapper.py b/android_env/wrappers/a11y_grpc_wrapper.py index b6e3d96..b58aad7 100644 --- a/android_env/wrappers/a11y_grpc_wrapper.py +++ b/android_env/wrappers/a11y_grpc_wrapper.py @@ -88,6 +88,7 @@ def __init__( max_enable_networking_attempts: int = 10, latest_a11y_info_only: bool = False, grpc_server_ip: str = '10.0.2.2', + port: int | None = None, ): """Initializes wrapper. @@ -121,6 +122,8 @@ def __init__( a11y info. By default, this is set to the IP address of the AVD's host machine which is 10.0.2.2: See https://developer.android.com/studio/run/emulator-networking#networkaddresses. + port: Port for the gRPC server. If not specified, a free port will be + allocated automatically. """ self._env = env self._grpc_server_ip = grpc_server_ip @@ -141,7 +144,7 @@ def __init__( self._servicer, self._server ) server_credentials = grpc.local_server_credentials() - self._port = portpicker.pick_unused_port() + self._port = port or portpicker.pick_unused_port() logging.info('Using port %s', self._port) uri_address = f'[::]:{self._port}' self._server.add_secure_port(uri_address, server_credentials) diff --git a/android_env/wrappers/a11y_grpc_wrapper_test.py b/android_env/wrappers/a11y_grpc_wrapper_test.py index abe0901..269a2cc 100644 --- a/android_env/wrappers/a11y_grpc_wrapper_test.py +++ b/android_env/wrappers/a11y_grpc_wrapper_test.py @@ -29,6 +29,7 @@ import dm_env import grpc import numpy as np +import portpicker def empty_forest() -> ( @@ -400,6 +401,42 @@ def test_configure_grpc( wrapped_env.reset() mock_configure_grpc.assert_called_once() + @mock.patch.object( + a11y_pb2_grpc, 'add_A11yServiceServicer_to_server', autospec=True + ) + @mock.patch.object(portpicker, 'pick_unused_port', return_value=1111) + @mock.patch.object(grpc, 'server', autospec=True) + def test_custom_port( + self, + mock_grpc_server, + mock_pick_unused_port, + unused_mock_add_servicer, + ): + base_env = mock.create_autospec( + env_interface.AndroidEnvInterface, instance=True + ) + base_env.stats.return_value = {'relaunch_count': 0} + base_env.execute_adb_call.return_value = _ok_response() + + # Case 1: Pass a custom port. It should use it and NOT call portpicker. + wrapped_env = a11y_grpc_wrapper.A11yGrpcWrapper(base_env, port=2222) + self.assertEqual(wrapped_env.get_port(), 2222) + mock_pick_unused_port.assert_not_called() + mock_grpc_server.return_value.add_secure_port.assert_called_with( + '[::]:2222', mock.ANY + ) + + # Reset mock for next case + mock_grpc_server.return_value.add_secure_port.reset_mock() + + # Case 2: Do not pass a port. It should call portpicker. + wrapped_env_default = a11y_grpc_wrapper.A11yGrpcWrapper(base_env) + self.assertEqual(wrapped_env_default.get_port(), 1111) + mock_pick_unused_port.assert_called_once() + mock_grpc_server.return_value.add_secure_port.assert_called_with( + '[::]:1111', mock.ANY + ) + @mock.patch.object( a11y_pb2_grpc, 'add_A11yServiceServicer_to_server', autospec=True )