diff --git a/android_env/components/adb_call_parser.py b/android_env/components/adb_call_parser.py index c38bf3f..ee401e2 100644 --- a/android_env/components/adb_call_parser.py +++ b/android_env/components/adb_call_parser.py @@ -15,6 +15,7 @@ """Processes adb_pb2.AdbRequest commands.""" +import datetime import os import re import subprocess @@ -63,7 +64,7 @@ def __init__(self, adb_controller: adb_control.AdbController): } def _execute_command( - self, command_args: list[str], timeout: float | None + self, command_args: list[str], timeout: datetime.timedelta | None ) -> tuple[adb_pb2.AdbResponse, bytes]: """Executes the command, catches errors and populates the response status. @@ -107,11 +108,17 @@ def parse(self, request: adb_pb2.AdbRequest) -> adb_pb2.AdbResponse: f'Got: {request.timeout_sec}') return response - timeout: float | None = request.timeout_sec or None + timeout = ( + datetime.timedelta(seconds=request.timeout_sec) + if request.timeout_sec > 0 + else None + ) return self._handlers[command_type](request, timeout) def _force_stop( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Stops an application. @@ -136,7 +143,7 @@ def _force_stop( return response def _fetch_current_task_id( - self, full_activity_name: str, timeout: float | None = None + self, full_activity_name: str, timeout: datetime.timedelta | None = None ) -> int: """Returns the task ID of the given `full_activity_name`. @@ -183,7 +190,9 @@ def _fetch_current_task_id( return -1 def _start_screen_pinning( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Pins an application. @@ -217,7 +226,9 @@ def _start_screen_pinning( return response def _send_broadcast( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Sends a broadcast. @@ -250,7 +261,9 @@ def _send_broadcast( return response def _install_apk( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Installs an app given its local path in the filesystem. @@ -305,7 +318,9 @@ def _install_apk( return response def _start_activity( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Starts a given activity. @@ -353,7 +368,9 @@ def _start_activity( return response def _press_button( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Presses a keyboard key. @@ -380,7 +397,9 @@ def _press_button( return response def _handle_uninstall_package( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Handles UninstallPackage messages. @@ -419,7 +438,9 @@ def _handle_uninstall_package( return response def _get_current_activity( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Fetches current activity. @@ -473,7 +494,9 @@ def _get_current_activity( return response def _get_orientation( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Fetches current device orientation. @@ -534,7 +557,9 @@ def _get_orientation( return response def _push( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Uploads contents to the device. @@ -565,7 +590,9 @@ def _push( return response def _pull( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Downloads file content from the device. @@ -598,7 +625,9 @@ def _pull( return response def _input_text( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Inserts text as keyboard events. @@ -621,7 +650,9 @@ def _input_text( return response def _tap( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Taps the device screen. @@ -651,7 +682,9 @@ def _tap( return response def _handle_settings( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Handles SettingsRequest messages. @@ -767,7 +800,9 @@ def _handle_settings( ) def _handle_generic( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Handles GenericRequest messages. @@ -786,7 +821,9 @@ def _handle_generic( return response def _handle_package_manager( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Handles PackageManagerRequest messages. @@ -880,7 +917,9 @@ def _handle_package_manager( ) def _handle_dumpsys( - self, request: adb_pb2.AdbRequest, timeout: float | None = None + self, + request: adb_pb2.AdbRequest, + timeout: datetime.timedelta | None = None, ) -> adb_pb2.AdbResponse: """Handles DumpsysRequest messages. diff --git a/android_env/components/adb_call_parser_test.py b/android_env/components/adb_call_parser_test.py index c3f4cfb..5c62aec 100644 --- a/android_env/components/adb_call_parser_test.py +++ b/android_env/components/adb_call_parser_test.py @@ -14,6 +14,7 @@ # limitations under the License. import builtins +import datetime import os import subprocess import sys @@ -775,6 +776,24 @@ def test_generic_command(self): self.assertEqual(response.generic.output, expected_output) adb.execute_command.assert_called_once_with(args, None) + def test_generic_command_with_timeout_successful(self): + adb = mock.create_autospec(adb_controller.AdbController) + expected_output = b'broadcast_output' + args = ['shell', 'am', 'broadcast', '-n', 'receiver', '-a', 'action'] + adb.execute_command.return_value = expected_output + parser = adb_call_parser.AdbCallParser(adb) + + generic_request = adb_pb2.AdbRequest.GenericRequest(args=args) + request = adb_pb2.AdbRequest(generic=generic_request, timeout_sec=5.0) + response = parser.parse(request) + + self.assertEqual(adb_pb2.AdbResponse.Status.OK, response.status) + self.assertEmpty(response.error_message) + self.assertEqual(response.generic.output, expected_output) + adb.execute_command.assert_called_once_with( + args, datetime.timedelta(seconds=5.0) + ) + def test_generic_command_adb_error(self): adb = mock.create_autospec(adb_controller.AdbController) args = ['shell', 'am', 'broadcast', '-n', 'receiver', '-a', 'action'] diff --git a/android_env/components/adb_controller.py b/android_env/components/adb_controller.py index 8273757..c68b332 100644 --- a/android_env/components/adb_controller.py +++ b/android_env/components/adb_controller.py @@ -15,6 +15,7 @@ """A class to manage and control an external ADB process.""" +import datetime import os import subprocess import time @@ -68,7 +69,7 @@ def command_prefix(self, include_device_name: bool = True) -> list[str]: command_prefix.extend(['-s', self._config.device_name]) return command_prefix - def init_server(self, timeout: float | None = None): + def init_server(self, timeout: datetime.timedelta | None = None): """Initialize the ADB server deamon on the given port. This function should be called immediately after initializing the first @@ -82,7 +83,7 @@ def init_server(self, timeout: float | None = None): self.execute_command(['devices'], timeout, device_specific=False) time.sleep(0.2) - def _restart_server(self, timeout: float | None = None): + def _restart_server(self, timeout: datetime.timedelta | None = None): """Kills and restarts the adb server. Args: @@ -105,7 +106,7 @@ def _restart_server(self, timeout: float | None = None): def execute_command( self, args: list[str], - timeout: float | None = None, + timeout: datetime.timedelta | None = None, device_specific: bool = True, ) -> bytes: """Executes an adb command. @@ -141,14 +142,17 @@ def execute_command( ) from latest_error def _execute_attempt( - self, command: list[str], command_str: str, timeout: float + self, + command: list[str], + command_str: str, + timeout: datetime.timedelta | None, ) -> bytes: """Executes a single adb command attempt.""" logging.info('Executing ADB command: [%s]', command_str) cmd_output = subprocess.check_output( command, stderr=subprocess.STDOUT, - timeout=timeout, + timeout=timeout.total_seconds() if timeout is not None else None, env=self._os_env_vars, ) logging.debug('ADB command output: %s', cmd_output) diff --git a/android_env/components/adb_controller_test.py b/android_env/components/adb_controller_test.py index 71a3a4e..aeb0af2 100644 --- a/android_env/components/adb_controller_test.py +++ b/android_env/components/adb_controller_test.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import os import subprocess import time @@ -25,7 +26,7 @@ # Timeout to be used by default in tests below. Set to a small value to avoid # hanging on a failed test. -_TIMEOUT = 2 +_TIMEOUT = datetime.timedelta(seconds=2) class AdbControllerTest(absltest.TestCase): @@ -67,7 +68,7 @@ def test_init_server(self, mock_sleep, mock_check_output): mock_check_output.assert_called_once_with( ['my_adb', 'devices'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ) mock_sleep.assert_called_once() @@ -104,7 +105,7 @@ def test_init_server_with_adb_server_port_from_os_env( mock_check_output.assert_called_once_with( ['my_adb', 'devices'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ) mock_sleep.assert_called_once() @@ -138,31 +139,31 @@ def test_restart_server(self, mock_sleep, mock_check_output): mock.call( ['my_adb', '-s', 'awesome_device', 'my_command'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'kill-server'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'start-server'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'devices'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', '-s', 'awesome_device', 'my_command'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), ]) @@ -211,31 +212,31 @@ def test_invalid_command(self, mock_sleep, mock_check_output): mock.call( ['my_adb', '-s', 'awesome_device', 'my_command'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'kill-server'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'start-server'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', 'devices'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), mock.call( ['my_adb', '-s', 'awesome_device', 'my_command'], stderr=subprocess.STDOUT, - timeout=_TIMEOUT, + timeout=_TIMEOUT.total_seconds(), env=expected_env, ), ], diff --git a/android_env/components/config_classes.py b/android_env/components/config_classes.py index 528974f..72461fd 100644 --- a/android_env/components/config_classes.py +++ b/android_env/components/config_classes.py @@ -16,6 +16,7 @@ """Dataclass definitions used for instantiating AndroidEnv components.""" import dataclasses +import datetime import enum @@ -31,7 +32,7 @@ class AdbControllerConfig: # Port for adb server. adb_server_port: int = 5037 # Default timeout in seconds for internal commands. - default_timeout: float = 120.0 + default_timeout: datetime.timedelta = datetime.timedelta(seconds=120.0) # Name of the device to communicate with. device_name: str = '' # Whether to use adb server port set in OS Environment variables. @@ -84,7 +85,7 @@ class SimulatorConfig: # How often to (asynchronously) grab the screenshot from the simulator. # If <= 0, stepping the environment blocks on fetching the screenshot (the # environment is synchronous). - interaction_rate_sec: float = 0.0 + interaction_rate_sec: datetime.timedelta = datetime.timedelta(seconds=0.0) @enum.unique diff --git a/android_env/components/simulators/base_simulator.py b/android_env/components/simulators/base_simulator.py index 66b8e79..f7aee84 100644 --- a/android_env/components/simulators/base_simulator.py +++ b/android_env/components/simulators/base_simulator.py @@ -17,6 +17,7 @@ import abc from collections.abc import Callable +import datetime import threading import time @@ -85,7 +86,7 @@ def launch(self) -> None: ) from error # Start interaction thread. - if self._config.interaction_rate_sec > 0: + if self._config.interaction_rate_sec > datetime.timedelta(seconds=0): self._interaction_thread = InteractionThread( self._get_screenshot_impl, self._config.interaction_rate_sec ) @@ -151,7 +152,7 @@ def save_state( def get_screenshot(self) -> np.ndarray: """Returns pixels representing the current screenshot of the simulator.""" - if self._config.interaction_rate_sec > 0: + if self._config.interaction_rate_sec > datetime.timedelta(seconds=0): assert self._interaction_thread is not None return self._interaction_thread.screenshot() # Async mode. else: @@ -180,7 +181,7 @@ class InteractionThread(threading.Thread): def __init__( self, get_screenshot_fn: Callable[[], np.ndarray], - interaction_rate_sec: float, + interaction_rate_sec: datetime.timedelta, ): super().__init__() self._get_screenshot_fn = get_screenshot_fn @@ -189,15 +190,15 @@ def __init__( self._screenshot = self._get_screenshot_fn() def run(self): - last_read = time.time() + last_read = time.monotonic() while not self._should_stop.is_set(): self._screenshot = self._get_screenshot_fn() - now = time.time() - elapsed = now - last_read + now = time.monotonic() + elapsed = datetime.timedelta(seconds=now - last_read) last_read = now sleep_time = self._interaction_rate_sec - elapsed - if sleep_time > 0.0: - time.sleep(sleep_time) + if sleep_time > datetime.timedelta(seconds=0): + time.sleep(sleep_time.total_seconds()) logging.info('InteractionThread.run() finished.') def stop(self): diff --git a/android_env/components/simulators/base_simulator_test.py b/android_env/components/simulators/base_simulator_test.py index d0ee545..8c28295 100644 --- a/android_env/components/simulators/base_simulator_test.py +++ b/android_env/components/simulators/base_simulator_test.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import itertools import time from unittest import mock @@ -79,7 +80,9 @@ def test_get_screenshot_error_async(self): errors.ReadObservationError() ) simulator = fake_simulator.FakeSimulator( - config_classes.FakeSimulatorConfig(interaction_rate_sec=0.5) + config_classes.FakeSimulatorConfig( + interaction_rate_sec=datetime.timedelta(seconds=0.5) + ) ) with mock.patch.object( base_simulator, @@ -99,7 +102,7 @@ def test_get_screenshot_faster_than_screenshot_impl(self): """Return same screenshot when step is faster than the interaction rate.""" # Arrange. - slow_rate = 0.5 + slow_rate = datetime.timedelta(seconds=0.5) simulator = fake_simulator.FakeSimulator( config_classes.FakeSimulatorConfig(interaction_rate_sec=slow_rate) ) @@ -126,7 +129,7 @@ def test_get_screenshot_slower_than_screenshot_impl(self): """Return different screenshots when step slower than the interaction rate.""" # Arrange. - fast_rate = 0.01 + fast_rate = datetime.timedelta(seconds=0.01) simulator = fake_simulator.FakeSimulator( config_classes.FakeSimulatorConfig(interaction_rate_sec=fast_rate) ) @@ -155,7 +158,9 @@ def test_interaction_thread_closes_upon_relaunch(self): # Arrange. simulator = fake_simulator.FakeSimulator( - config_classes.FakeSimulatorConfig(interaction_rate_sec=0.01) + config_classes.FakeSimulatorConfig( + interaction_rate_sec=datetime.timedelta(seconds=0.01) + ) ) mock_interaction_thread = mock.create_autospec( base_simulator.InteractionThread diff --git a/android_env/components/simulators/emulator/emulator_simulator.py b/android_env/components/simulators/emulator/emulator_simulator.py index 4e7af5c..e49c60d 100644 --- a/android_env/components/simulators/emulator/emulator_simulator.py +++ b/android_env/components/simulators/emulator/emulator_simulator.py @@ -16,6 +16,7 @@ """A class that manages an Android Emulator.""" from collections.abc import Callable, Mapping +import datetime import functools import os import platform @@ -370,7 +371,7 @@ def save_state( def _connect_to_emulator( self, grpc_port: int, - timeout_sec: int = 100, + timeout: datetime.timedelta = datetime.timedelta(seconds=100), ) -> tuple[ emulator_controller_pb2_grpc.EmulatorControllerStub, snapshot_service_pb2_grpc.SnapshotServiceStub, @@ -390,7 +391,9 @@ def _connect_to_emulator( try: self._channel = grpc.secure_channel(port, creds, options=options) - grpc.channel_ready_future(self._channel).result(timeout=timeout_sec) + grpc.channel_ready_future(self._channel).result( + timeout=timeout.total_seconds() + ) except (grpc.RpcError, grpc.FutureTimeoutError) as grpc_error: logging.exception('Failed to connect to the emulator.') raise EmulatorBootError( @@ -404,16 +407,19 @@ def _connect_to_emulator( return emulator_controller_stub, snapshot_stub @_reconnect_on_grpc_error - def _confirm_booted(self, startup_wait_time_sec: int = 300): + def _confirm_booted( + self, + startup_wait_time: datetime.timedelta = datetime.timedelta(seconds=300), + ): """Waits until the emulator is fully booted.""" assert ( self._emulator_stub is not None ), 'Emulator stub has not been initialized yet.' - start_time = time.time() - deadline = start_time + startup_wait_time_sec + start_time = time.monotonic() + deadline = start_time + startup_wait_time.total_seconds() success = False - while time.time() < deadline: + while time.monotonic() < deadline: emu_status = self._emulator_stub.getStatus(empty_pb2.Empty()) logging.info('Waiting for emulator (%r) to start... (%rms)', self.adb_device_name(), emu_status.uptime) @@ -422,10 +428,12 @@ def _confirm_booted(self, startup_wait_time_sec: int = 300): break time.sleep(5.0) - elapsed_time = time.time() - start_time + elapsed_time = time.monotonic() - start_time if not success: raise EmulatorCrashError( - f'The emulator failed to boot after {startup_wait_time_sec} seconds') + 'The emulator failed to boot after' + f' {startup_wait_time.total_seconds()} seconds' + ) logging.info('Done booting the emulator (in %f seconds).', elapsed_time) logging.info('********** Emulator logs **********') diff --git a/android_env/components/simulators/emulator/emulator_simulator_test.py b/android_env/components/simulators/emulator/emulator_simulator_test.py index 708a274..8824d5d 100644 --- a/android_env/components/simulators/emulator/emulator_simulator_test.py +++ b/android_env/components/simulators/emulator/emulator_simulator_test.py @@ -16,6 +16,7 @@ """Tests for android_env.components.emulator_simulator.""" import builtins +import datetime import os import time from unittest import mock @@ -342,7 +343,7 @@ def test_get_screenshot(self): def test_get_screenshot_reconnects_on_grpc_error(self): config = config_classes.EmulatorConfig( - interaction_rate_sec=0.0, + interaction_rate_sec=datetime.timedelta(seconds=0.0), emulator_launcher=config_classes.EmulatorLauncherConfig( grpc_port=1234, tmp_dir=self.create_tempdir().full_path ), diff --git a/android_env/components/simulators/fake/fake_simulator.py b/android_env/components/simulators/fake/fake_simulator.py index 4209b17..8893e31 100644 --- a/android_env/components/simulators/fake/fake_simulator.py +++ b/android_env/components/simulators/fake/fake_simulator.py @@ -15,6 +15,7 @@ """Fake Simulator for testing AndroidEnv infrastructure.""" +import datetime import random import threading import time @@ -67,7 +68,7 @@ class FakeAdbController(adb_controller.AdbController): def execute_command( self, args: list[str], - timeout: float | None = None, + timeout: datetime.timedelta | None = None, device_specific: bool = True, ) -> bytes: """Returns fake output for adb commands."""