diff --git a/android_env/components/errors.py b/android_env/components/errors.py index 12de50d..17b5971 100644 --- a/android_env/components/errors.py +++ b/android_env/components/errors.py @@ -78,6 +78,12 @@ class CheckInstallError(StepCommandError): ERROR_CODE = 9 +class CheckDumpsysError(StepCommandError): + """Raised when the check_dumpsys success check is not met.""" + + ERROR_CODE = 10 + + def from_code(code: int, msg: str = '') -> AndroidEnvError | None: """Returns an AndroidEnvError instance from the given arguments.""" @@ -92,6 +98,7 @@ def from_code(code: int, msg: str = '') -> AndroidEnvError | None: 7: StepCommandError, 8: WaitForAppScreenError, 9: CheckInstallError, + 10: CheckDumpsysError, } if code in code_to_error: diff --git a/android_env/components/setup_step_interpreter.py b/android_env/components/setup_step_interpreter.py index 6894515..98e7c31 100644 --- a/android_env/components/setup_step_interpreter.py +++ b/android_env/components/setup_step_interpreter.py @@ -42,6 +42,7 @@ def __init__(self, adb_call_parser: adb_call_parser_lib.AdbCallParser): 'error_count_adb_request': 0, 'error_count_wait_for_app_screen': 0, 'error_count_check_install': 0, + 'error_count_check_dumpsys': 0, 'error_count_wait_for_message': 0, 'total_time_waiting_for_app_screen': 0.0, } @@ -107,6 +108,16 @@ def _process_step_command(self, step_cmd: task_pb2.SetupStep) -> None: success_condition.check_install.package_name, num_tries, max_retries) + except errors.CheckDumpsysError as error: + latest_error = error + self._stats['error_count_check_dumpsys'] += 1 + logging.exception( + 'Dumpsys check for service [%r] failed. Try %d of %d.', + success_condition.check_dumpsys.service, + num_tries, + max_retries, + ) + raise errors.StepCommandError( f'Step failed: [{step_cmd}]') from latest_error @@ -155,6 +166,8 @@ def _check_success( self._stats['total_time_waiting_for_app_screen'] += wait_time case 'check_install': self._check_install(success_condition.check_install) + case 'check_dumpsys': + self._check_dumpsys(success_condition.check_dumpsys) case _: raise NotImplementedError(f'No success check called [{success_check}].') @@ -180,3 +193,39 @@ def _check_install(self, check_install: task_pb2.CheckInstall) -> None: logging.error('Package not found.') raise errors.CheckInstallError() + + def _check_dumpsys(self, check_dumpsys: task_pb2.CheckDumpsys) -> None: + """Checks that the dumpsys output contains the expected string.""" + + service = check_dumpsys.service + expected_string = check_dumpsys.expected_string + logging.info( + 'Checking dumpsys output for service [%r] matching [%r]', + service, + expected_string, + ) + + request = adb_pb2.AdbRequest( + dumpsys=adb_pb2.AdbRequest.DumpsysRequest(service=service) + ) + + start_time = time.time() + while time.time() - start_time < check_dumpsys.timeout_sec: + response = self._adb_call_parser.parse(request) + if response.status == adb_pb2.AdbResponse.Status.OK: + output = response.dumpsys.output + if isinstance(output, bytes): + output = output.decode('utf-8', errors='replace') + if expected_string in output: + logging.info( + 'Done confirming dumpsys output for service [%r].', service + ) + return + time.sleep(0.1) + + logging.error( + 'Expected string [%r] not found in dumpsys output for service [%r].', + expected_string, + service, + ) + raise errors.CheckDumpsysError() diff --git a/android_env/components/setup_step_interpreter_test.py b/android_env/components/setup_step_interpreter_test.py index 63e0524..1e612af 100644 --- a/android_env/components/setup_step_interpreter_test.py +++ b/android_env/components/setup_step_interpreter_test.py @@ -15,6 +15,7 @@ """Tests for android_env.components.setup_step_interpreter.""" +import time from unittest import mock from absl.testing import absltest @@ -336,6 +337,62 @@ def test_retry_step(self): # We expect the check to fail once and succeed on the second pass. self.assertEqual(self._parser.parse.call_count, 2) + def test_check_dumpsys_success(self): + self._parser.parse.return_value = adb_pb2.AdbResponse( + status=adb_pb2.AdbResponse.Status.OK, + dumpsys=adb_pb2.AdbResponse.DumpsysResponse( + output=b'Service accessibility: InputService is running' + ), + ) + interpreter = setup_step_interpreter.SetupStepInterpreter( + adb_call_parser=self._parser + ) + interpreter.interpret([ + _to_proto( + task_pb2.SetupStep, + """ +success_condition: { + check_dumpsys: { + service: "accessibility" + expected_string: "InputService" + timeout_sec: 0.0001 + } +}""", + ) + ]) + self._parser.parse.assert_called_once_with( + adb_pb2.AdbRequest( + dumpsys=adb_pb2.AdbRequest.DumpsysRequest(service='accessibility') + ) + ) + + @mock.patch.object(time, 'sleep', autospec=True) + def test_check_dumpsys_failure(self, unused_mock_sleep): + self._parser.parse.return_value = adb_pb2.AdbResponse( + status=adb_pb2.AdbResponse.Status.OK, + dumpsys=adb_pb2.AdbResponse.DumpsysResponse( + output=b'Service accessibility: OtherService' + ), + ) + interpreter = setup_step_interpreter.SetupStepInterpreter( + adb_call_parser=self._parser + ) + with self.assertRaises(errors.StepCommandError): + interpreter.interpret([ + _to_proto( + task_pb2.SetupStep, + """ +success_condition: { + check_dumpsys: { + service: "accessibility" + expected_string: "InputService" + timeout_sec: 0.0001 + } +}""", + ) + ]) + self.assertEqual(interpreter.stats()['error_count_check_dumpsys'], 3) + if __name__ == '__main__': absltest.main() diff --git a/android_env/proto/task.proto b/android_env/proto/task.proto index c4b5431..4a05782 100644 --- a/android_env/proto/task.proto +++ b/android_env/proto/task.proto @@ -52,6 +52,12 @@ message CheckInstall { float timeout_sec = 2; } +message CheckDumpsys { + string service = 1; // e.g. "accessibility" + string expected_string = 2; // e.g. "InputService" + float timeout_sec = 3; +} + message Sleep { float time_sec = 1; } @@ -62,6 +68,7 @@ message SuccessCondition { oneof check { WaitForAppScreen wait_for_app_screen = 2; CheckInstall check_install = 3; + CheckDumpsys check_dumpsys = 4; } }