android-env 1.2.1__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. android_env/__init__.py +1 -1
  2. android_env/components/__init__.py +1 -1
  3. android_env/components/a11y/__init__.py +15 -0
  4. android_env/components/a11y/a11y_events.py +118 -0
  5. android_env/components/a11y/a11y_events_test.py +173 -0
  6. android_env/components/a11y/a11y_forests.py +128 -0
  7. android_env/components/a11y/a11y_forests_test.py +237 -0
  8. android_env/components/a11y/a11y_servicer.py +199 -0
  9. android_env/components/a11y/a11y_servicer_test.py +224 -0
  10. android_env/components/action_fns.py +132 -0
  11. android_env/components/action_fns_test.py +227 -0
  12. android_env/components/action_type.py +26 -3
  13. android_env/components/adb_call_parser.py +239 -196
  14. android_env/components/adb_call_parser_test.py +179 -209
  15. android_env/components/adb_controller.py +90 -52
  16. android_env/components/adb_controller_test.py +187 -16
  17. android_env/components/adb_log_stream.py +17 -5
  18. android_env/components/adb_log_stream_test.py +17 -3
  19. android_env/components/app_screen_checker.py +17 -15
  20. android_env/components/app_screen_checker_test.py +7 -8
  21. android_env/components/config_classes.py +203 -0
  22. android_env/components/coordinator.py +102 -338
  23. android_env/components/coordinator_test.py +59 -199
  24. android_env/components/device_settings.py +174 -0
  25. android_env/components/device_settings_test.py +228 -0
  26. android_env/components/dumpsys_thread.py +3 -4
  27. android_env/components/dumpsys_thread_test.py +1 -1
  28. android_env/components/errors.py +52 -10
  29. android_env/components/errors_test.py +110 -0
  30. android_env/components/log_stream.py +7 -5
  31. android_env/components/log_stream_test.py +1 -1
  32. android_env/components/logcat_thread.py +9 -8
  33. android_env/components/logcat_thread_test.py +3 -4
  34. android_env/components/{utils.py → pixel_fns.py} +20 -20
  35. android_env/components/{utils_test.py → pixel_fns_test.py} +20 -15
  36. android_env/components/setup_step_interpreter.py +47 -39
  37. android_env/components/setup_step_interpreter_test.py +4 -4
  38. android_env/components/simulators/__init__.py +1 -1
  39. android_env/components/simulators/base_simulator.py +116 -44
  40. android_env/components/simulators/base_simulator_test.py +131 -9
  41. android_env/components/simulators/emulator/__init__.py +1 -1
  42. android_env/components/simulators/emulator/emulator_launcher.py +67 -77
  43. android_env/components/simulators/emulator/emulator_launcher_test.py +153 -49
  44. android_env/components/simulators/emulator/emulator_simulator.py +276 -95
  45. android_env/components/simulators/emulator/emulator_simulator_test.py +314 -89
  46. android_env/components/simulators/fake/__init__.py +1 -1
  47. android_env/components/simulators/fake/fake_simulator.py +17 -25
  48. android_env/components/simulators/fake/fake_simulator_test.py +29 -12
  49. android_env/components/specs.py +18 -28
  50. android_env/components/specs_test.py +1 -44
  51. android_env/components/task_manager.py +48 -48
  52. android_env/components/task_manager_test.py +71 -60
  53. android_env/env_interface.py +37 -23
  54. android_env/environment.py +83 -51
  55. android_env/environment_test.py +68 -29
  56. android_env/loader.py +57 -43
  57. android_env/loader_test.py +115 -35
  58. android_env/proto/__init__.py +1 -1
  59. android_env/proto/a11y/__init__.py +15 -0
  60. android_env/proto/a11y/a11y.proto +75 -0
  61. android_env/proto/a11y/a11y_pb2.py +54 -0
  62. android_env/proto/a11y/a11y_pb2.pyi +49 -0
  63. android_env/proto/a11y/a11y_pb2_grpc.py +202 -0
  64. android_env/proto/a11y/android_accessibility_action.proto +32 -0
  65. android_env/proto/a11y/android_accessibility_action_pb2.py +37 -0
  66. android_env/proto/a11y/android_accessibility_action_pb2.pyi +13 -0
  67. android_env/proto/a11y/android_accessibility_action_pb2_grpc.py +24 -0
  68. android_env/proto/a11y/android_accessibility_forest.proto +29 -0
  69. android_env/proto/a11y/android_accessibility_forest_pb2.py +38 -0
  70. android_env/proto/a11y/android_accessibility_forest_pb2.pyi +13 -0
  71. android_env/proto/a11y/android_accessibility_forest_pb2_grpc.py +24 -0
  72. android_env/proto/a11y/android_accessibility_node_info.proto +122 -0
  73. android_env/proto/a11y/android_accessibility_node_info_clickable_span.proto +49 -0
  74. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.py +39 -0
  75. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.pyi +28 -0
  76. android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2_grpc.py +24 -0
  77. android_env/proto/a11y/android_accessibility_node_info_pb2.py +42 -0
  78. android_env/proto/a11y/android_accessibility_node_info_pb2.pyi +75 -0
  79. android_env/proto/a11y/android_accessibility_node_info_pb2_grpc.py +24 -0
  80. android_env/proto/a11y/android_accessibility_tree.proto +29 -0
  81. android_env/proto/a11y/android_accessibility_tree_pb2.py +38 -0
  82. android_env/proto/a11y/android_accessibility_tree_pb2.pyi +13 -0
  83. android_env/proto/a11y/android_accessibility_tree_pb2_grpc.py +24 -0
  84. android_env/proto/a11y/android_accessibility_window_info.proto +84 -0
  85. android_env/proto/a11y/android_accessibility_window_info_pb2.py +41 -0
  86. android_env/proto/a11y/android_accessibility_window_info_pb2.pyi +48 -0
  87. android_env/proto/a11y/android_accessibility_window_info_pb2_grpc.py +24 -0
  88. android_env/proto/a11y/rect.proto +30 -0
  89. android_env/proto/a11y/rect_pb2.py +37 -0
  90. android_env/proto/a11y/rect_pb2.pyi +17 -0
  91. android_env/proto/a11y/rect_pb2_grpc.py +24 -0
  92. android_env/proto/adb.proto +17 -6
  93. android_env/proto/adb_pb2.py +120 -107
  94. android_env/proto/adb_pb2.pyi +396 -0
  95. android_env/proto/adb_pb2_grpc.py +20 -0
  96. android_env/proto/emulator_controller.proto +68 -63
  97. android_env/proto/emulator_controller_pb2.py +142 -131
  98. android_env/proto/emulator_controller_pb2.pyi +672 -0
  99. android_env/proto/emulator_controller_pb2_grpc.py +505 -142
  100. android_env/proto/snapshot.proto +169 -0
  101. android_env/proto/snapshot_pb2.py +47 -0
  102. android_env/proto/snapshot_pb2.pyi +117 -0
  103. android_env/proto/snapshot_pb2_grpc.py +24 -0
  104. android_env/proto/snapshot_service.proto +289 -0
  105. android_env/proto/snapshot_service_pb2.py +54 -0
  106. android_env/proto/snapshot_service_pb2.pyi +86 -0
  107. android_env/proto/snapshot_service_pb2_grpc.py +487 -0
  108. android_env/proto/state.proto +63 -0
  109. android_env/proto/state_pb2.py +63 -0
  110. android_env/proto/state_pb2.pyi +85 -0
  111. android_env/proto/state_pb2_grpc.py +24 -0
  112. android_env/proto/task.proto +5 -1
  113. android_env/proto/task_pb2.py +42 -31
  114. android_env/proto/task_pb2.pyi +160 -0
  115. android_env/proto/task_pb2_grpc.py +20 -0
  116. android_env/wrappers/__init__.py +1 -1
  117. android_env/wrappers/a11y_grpc_wrapper.py +500 -0
  118. android_env/wrappers/a11y_grpc_wrapper_test.py +849 -0
  119. android_env/wrappers/base_wrapper.py +34 -13
  120. android_env/wrappers/base_wrapper_test.py +22 -16
  121. android_env/wrappers/discrete_action_wrapper.py +18 -17
  122. android_env/wrappers/discrete_action_wrapper_test.py +4 -4
  123. android_env/wrappers/flat_interface_wrapper.py +5 -5
  124. android_env/wrappers/flat_interface_wrapper_test.py +7 -11
  125. android_env/wrappers/float_pixels_wrapper.py +9 -10
  126. android_env/wrappers/float_pixels_wrapper_test.py +3 -3
  127. android_env/wrappers/gym_wrapper.py +19 -13
  128. android_env/wrappers/gym_wrapper_test.py +3 -5
  129. android_env/wrappers/image_rescale_wrapper.py +18 -21
  130. android_env/wrappers/image_rescale_wrapper_test.py +25 -37
  131. android_env/wrappers/last_action_wrapper.py +16 -13
  132. android_env/wrappers/last_action_wrapper_test.py +44 -51
  133. android_env/wrappers/rate_limit_wrapper.py +6 -3
  134. android_env/wrappers/rate_limit_wrapper_test.py +22 -1
  135. android_env/wrappers/tap_action_wrapper.py +16 -17
  136. android_env/wrappers/tap_action_wrapper_test.py +51 -16
  137. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/METADATA +14 -18
  138. android_env-1.2.3.dist-info/RECORD +141 -0
  139. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/WHEEL +1 -1
  140. android_env/proto/raw_observation.proto +0 -39
  141. android_env/proto/raw_observation_pb2.py +0 -27
  142. android_env/proto/raw_observation_pb2_grpc.py +0 -4
  143. android_env-1.2.1.dist-info/RECORD +0 -81
  144. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info/licenses}/LICENSE +0 -0
  145. {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -18,33 +18,20 @@
18
18
  import os
19
19
  import subprocess
20
20
  import time
21
- from typing import List, Optional
22
21
 
23
22
  from absl import logging
23
+ from android_env.components import config_classes
24
24
  from android_env.components import errors
25
25
 
26
26
 
27
- class AdbController():
27
+ class AdbController:
28
28
  """Manages communication with adb."""
29
29
 
30
- def __init__(self,
31
- device_name: str = '',
32
- adb_path: str = 'adb',
33
- adb_server_port: int = 5037,
34
- default_timeout: float = 120.0):
35
- """Instantiates an AdbController object.
30
+ def __init__(self, config: config_classes.AdbControllerConfig):
31
+ """Instantiates an AdbController object."""
36
32
 
37
- Args:
38
- device_name: Name of the device to communicate with.
39
- adb_path: Path to the adb binary.
40
- adb_server_port: Port for adb server.
41
- default_timeout: Default timeout in seconds.
42
- """
43
-
44
- self._device_name = device_name
45
- self._adb_path = adb_path
46
- self._adb_server_port = str(adb_server_port)
47
- self._default_timeout = default_timeout
33
+ self._config = config
34
+ logging.info('config: %r', self._config)
48
35
 
49
36
  # Unset problematic environment variables. ADB commands will fail if these
50
37
  # are set. They are normally exported by AndroidStudio.
@@ -53,14 +40,25 @@ class AdbController():
53
40
  if 'ANDROID_ADB_SERVER_PORT' in os.environ:
54
41
  del os.environ['ANDROID_ADB_SERVER_PORT']
55
42
 
56
- def command_prefix(self) -> List[str]:
43
+ # Explicitly expand the $HOME environment variable.
44
+ self._os_env_vars = dict(os.environ).copy()
45
+ self._os_env_vars.update(
46
+ {'HOME': os.path.expandvars(self._os_env_vars.get('HOME', ''))}
47
+ )
48
+ logging.info('self._os_env_vars: %r', self._os_env_vars)
49
+
50
+ def command_prefix(self, include_device_name: bool = True) -> list[str]:
57
51
  """The command for instantiating an adb client to this server."""
58
- command_prefix = [self._adb_path, '-P', self._adb_server_port]
59
- if self._device_name:
60
- command_prefix.extend(['-s', self._device_name])
52
+ command_prefix = [
53
+ self._config.adb_path,
54
+ '-P',
55
+ str(self._config.adb_server_port),
56
+ ]
57
+ if include_device_name:
58
+ command_prefix.extend(['-s', self._config.device_name])
61
59
  return command_prefix
62
60
 
63
- def init_server(self, timeout: Optional[float] = None):
61
+ def init_server(self, timeout: float | None = None):
64
62
  """Initialize the ADB server deamon on the given port.
65
63
 
66
64
  This function should be called immediately after initializing the first
@@ -71,16 +69,34 @@ class AdbController():
71
69
  timeout set on the constructor will be used.
72
70
  """
73
71
  # Make an initial device-independent call to ADB to start the deamon.
74
- device_name_tmp = self._device_name
75
- self._device_name = ''
76
- self.execute_command(['devices'], timeout=timeout)
72
+ self.execute_command(['devices'], timeout, device_specific=False)
73
+ time.sleep(0.2)
74
+
75
+ def _restart_server(self, timeout: float | None = None):
76
+ """Kills and restarts the adb server.
77
+
78
+ Args:
79
+ timeout: A timeout to use for this operation. If not set the default
80
+ timeout set on the constructor will be used.
81
+ """
82
+ logging.info('Restarting adb server.')
83
+ self.execute_command(
84
+ ['kill-server'], timeout=timeout, device_specific=False)
85
+ time.sleep(0.2)
86
+ cmd_output = self.execute_command(
87
+ ['start-server'], timeout=timeout, device_specific=False)
88
+ logging.info('start-server output: %r', cmd_output.decode('utf-8'))
89
+ time.sleep(2.0)
90
+ self.execute_command(
91
+ ['devices'], timeout=timeout, device_specific=False)
77
92
  time.sleep(0.2)
78
- # Subsequent calls will use the device name.
79
- self._device_name = device_name_tmp
80
93
 
81
- def execute_command(self,
82
- args: List[str],
83
- timeout: Optional[float] = None) -> bytes:
94
+ def execute_command(
95
+ self,
96
+ args: list[str],
97
+ timeout: float | None = None,
98
+ device_specific: bool = True,
99
+ ) -> bytes:
84
100
  """Executes an adb command.
85
101
 
86
102
  Args:
@@ -88,26 +104,48 @@ class AdbController():
88
104
  For example: ['install', '/my/app.apk']
89
105
  timeout: A timeout to use for this operation. If not set the default
90
106
  timeout set on the constructor will be used.
107
+ device_specific: Whether the call is device-specific or independent.
91
108
 
92
109
  Returns:
93
110
  The output of running such command as a binary string.
94
111
  """
95
- timeout = self._default_timeout if timeout is None else timeout
96
- command = self.command_prefix() + args
97
- command_str = ' '.join(command)
98
- logging.info('Executing ADB command: [%s]', command_str)
99
-
100
- try:
101
- cmd_output = subprocess.check_output(
102
- command, stderr=subprocess.STDOUT, timeout=timeout)
103
- logging.info('Done executing ADB command: [%s]', command_str)
104
- return cmd_output
105
- except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error:
106
- if error.stdout is not None:
107
- logging.error('**stdout**:')
108
- for line in error.stdout.splitlines():
109
- logging.error(line)
110
- raise errors.AdbControllerError(
111
- f'Error executing adb command: [{command_str}]\n'
112
- f'Caused by {error}\n'
113
- f'adb output: [{error.stdout}]') from error
112
+ timeout = self._config.default_timeout if timeout is None else timeout
113
+ command = self.command_prefix(include_device_name=device_specific) + args
114
+ command_str = 'adb ' + ' '.join(command[1:])
115
+
116
+ n_retries = 2
117
+ n_tries = 1
118
+ latest_error = None
119
+ while n_tries <= n_retries:
120
+ try:
121
+ logging.info('Executing ADB command: [%s]', command_str)
122
+ cmd_output = subprocess.check_output(
123
+ command,
124
+ stderr=subprocess.STDOUT,
125
+ timeout=timeout,
126
+ env=self._os_env_vars,
127
+ )
128
+ logging.debug('ADB command output: %s', cmd_output)
129
+ return cmd_output
130
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
131
+ logging.exception(
132
+ 'Failed to execute ADB command (try %r of 3): [%s]',
133
+ n_tries, command_str)
134
+ if e.stdout is not None:
135
+ logging.error('**stdout**:')
136
+ for line in e.stdout.splitlines():
137
+ logging.error(' %s', line)
138
+ if e.stderr is not None:
139
+ logging.error('**stderr**:')
140
+ for line in e.stderr.splitlines():
141
+ logging.error(' %s', line)
142
+ n_tries += 1
143
+ latest_error = e
144
+ if device_specific and n_tries <= n_retries:
145
+ self._restart_server(timeout=timeout)
146
+
147
+ raise errors.AdbControllerError(
148
+ f'Error executing adb command: [{command_str}]\n'
149
+ f'Caused by: {latest_error}\n'
150
+ f'adb stdout: [{latest_error.stdout}]\n'
151
+ f'adb stderr: [{latest_error.stderr}]') from latest_error
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -16,11 +16,14 @@
16
16
  """Tests for android_env.components.adb_controller."""
17
17
 
18
18
  import os
19
+ import subprocess
19
20
  import time
20
21
  from unittest import mock
21
22
 
22
23
  from absl.testing import absltest
23
- from android_env.components import adb_controller
24
+ from android_env.components import adb_controller as adb_controller_lib
25
+ from android_env.components import config_classes
26
+ from android_env.components import errors
24
27
 
25
28
  # Timeout to be used by default in tests below. Set to a small value to avoid
26
29
  # hanging on a failed test.
@@ -31,30 +34,198 @@ class AdbControllerTest(absltest.TestCase):
31
34
 
32
35
  def setUp(self):
33
36
  super().setUp()
34
- self._mock_execute_command = self.enter_context(
35
- mock.patch.object(
36
- adb_controller.AdbController, 'execute_command', autospec=True))
37
- self._adb_controller = adb_controller.AdbController(
38
- adb_path='my_adb', device_name='awesome_device', adb_server_port=9999)
37
+ # Set two env vars.
38
+ os.environ['MY_ENV_VAR'] = '/some/path/'
39
+ os.environ['HOME'] = '$MY_ENV_VAR'
40
+ self._env_before = os.environ
41
+ self._adb_controller = adb_controller_lib.AdbController(
42
+ config_classes.AdbControllerConfig(
43
+ adb_path='my_adb',
44
+ device_name='awesome_device',
45
+ adb_server_port=9999,
46
+ )
47
+ )
39
48
 
49
+ @mock.patch.object(subprocess, 'check_output', autospec=True)
40
50
  @mock.patch.object(time, 'sleep', autospec=True)
41
- def test_init_server(self, mock_sleep):
42
- self._adb_controller.init_server(timeout=_TIMEOUT)
43
- self._mock_execute_command.assert_called_once_with(self._adb_controller,
44
- ['devices'], _TIMEOUT)
51
+ def test_init_server(self, mock_sleep, mock_check_output):
52
+ # Arrange.
53
+ adb_controller = adb_controller_lib.AdbController(
54
+ config_classes.AdbControllerConfig(
55
+ adb_path='my_adb',
56
+ device_name='awesome_device',
57
+ adb_server_port=9999,
58
+ )
59
+ )
60
+
61
+ # Act.
62
+ adb_controller.init_server(timeout=_TIMEOUT)
63
+
64
+ # Assert.
65
+ expected_env = self._env_before
66
+ expected_env['HOME'] = '/some/path/'
67
+ mock_check_output.assert_called_once_with(
68
+ ['my_adb', '-P', '9999', 'devices'],
69
+ stderr=subprocess.STDOUT,
70
+ timeout=_TIMEOUT,
71
+ env=expected_env,
72
+ )
45
73
  mock_sleep.assert_called_once()
46
74
 
75
+ @mock.patch.object(subprocess, 'check_output', autospec=True)
76
+ @mock.patch.object(time, 'sleep', autospec=True)
77
+ def test_restart_server(self, mock_sleep, mock_check_output):
78
+ # Arrange.
79
+ mock_check_output.side_effect = [
80
+ subprocess.CalledProcessError(returncode=1, cmd='blah'),
81
+ ] + ['fake_output'.encode('utf-8')] * 4
82
+ adb_controller = adb_controller_lib.AdbController(
83
+ config_classes.AdbControllerConfig(
84
+ adb_path='my_adb',
85
+ device_name='awesome_device',
86
+ adb_server_port=9999,
87
+ )
88
+ )
89
+
90
+ # Act.
91
+ adb_controller.execute_command(['my_command'], timeout=_TIMEOUT)
92
+
93
+ # Assert.
94
+ expected_env = self._env_before
95
+ expected_env['HOME'] = '/some/path/'
96
+ mock_check_output.assert_has_calls([
97
+ mock.call(
98
+ ['my_adb', '-P', '9999', '-s', 'awesome_device', 'my_command'],
99
+ stderr=subprocess.STDOUT,
100
+ timeout=_TIMEOUT,
101
+ env=expected_env,
102
+ ),
103
+ mock.call(
104
+ ['my_adb', '-P', '9999', 'kill-server'],
105
+ stderr=subprocess.STDOUT,
106
+ timeout=_TIMEOUT,
107
+ env=expected_env,
108
+ ),
109
+ mock.call(
110
+ ['my_adb', '-P', '9999', 'start-server'],
111
+ stderr=subprocess.STDOUT,
112
+ timeout=_TIMEOUT,
113
+ env=expected_env,
114
+ ),
115
+ mock.call(
116
+ ['my_adb', '-P', '9999', 'devices'],
117
+ stderr=subprocess.STDOUT,
118
+ timeout=_TIMEOUT,
119
+ env=expected_env,
120
+ ),
121
+ mock.call(
122
+ ['my_adb', '-P', '9999', '-s', 'awesome_device', 'my_command'],
123
+ stderr=subprocess.STDOUT,
124
+ timeout=_TIMEOUT,
125
+ env=expected_env,
126
+ ),
127
+ ])
128
+ mock_sleep.assert_has_calls(
129
+ [mock.call(0.2), mock.call(2.0), mock.call(0.2)])
130
+
131
+ @mock.patch.object(subprocess, 'check_output', autospec=True)
132
+ @mock.patch.object(time, 'sleep', autospec=True)
133
+ def test_invalid_command(self, mock_sleep, mock_check_output):
134
+ # Arrange.
135
+ restart_sequence = ['fake_output'.encode('utf-8')] * 3
136
+ mock_check_output.side_effect = (
137
+ [
138
+ subprocess.CalledProcessError(returncode=1, cmd='blah'),
139
+ ]
140
+ + restart_sequence
141
+ + [subprocess.CalledProcessError(returncode=1, cmd='blah')]
142
+ # Don't restart if last call fails.
143
+ )
144
+ adb_controller = adb_controller_lib.AdbController(
145
+ config_classes.AdbControllerConfig(
146
+ adb_path='my_adb',
147
+ device_name='awesome_device',
148
+ adb_server_port=9999,
149
+ )
150
+ )
151
+
152
+ # Act.
153
+ with self.assertRaises(errors.AdbControllerError):
154
+ adb_controller.execute_command(['my_command'], timeout=_TIMEOUT)
155
+
156
+ # Assert.
157
+ expected_env = self._env_before
158
+ expected_env['HOME'] = '/some/path/'
159
+ mock_check_output.assert_has_calls(
160
+ [
161
+ mock.call(
162
+ ['my_adb', '-P', '9999', '-s', 'awesome_device', 'my_command'],
163
+ stderr=subprocess.STDOUT,
164
+ timeout=_TIMEOUT,
165
+ env=expected_env,
166
+ ),
167
+ mock.call(
168
+ ['my_adb', '-P', '9999', 'kill-server'],
169
+ stderr=subprocess.STDOUT,
170
+ timeout=_TIMEOUT,
171
+ env=expected_env,
172
+ ),
173
+ mock.call(
174
+ ['my_adb', '-P', '9999', 'start-server'],
175
+ stderr=subprocess.STDOUT,
176
+ timeout=_TIMEOUT,
177
+ env=expected_env,
178
+ ),
179
+ mock.call(
180
+ ['my_adb', '-P', '9999', 'devices'],
181
+ stderr=subprocess.STDOUT,
182
+ timeout=_TIMEOUT,
183
+ env=expected_env,
184
+ ),
185
+ mock.call(
186
+ ['my_adb', '-P', '9999', '-s', 'awesome_device', 'my_command'],
187
+ stderr=subprocess.STDOUT,
188
+ timeout=_TIMEOUT,
189
+ env=expected_env,
190
+ ),
191
+ ],
192
+ any_order=False,
193
+ )
194
+ mock_sleep.assert_has_calls(
195
+ [mock.call(0.2), mock.call(2.0), mock.call(0.2)]
196
+ )
197
+
198
+ @mock.patch.object(subprocess, 'check_output', autospec=True)
199
+ @mock.patch.object(time, 'sleep', autospec=True)
200
+ def test_avoid_infinite_recursion(self, mock_sleep, mock_check_output):
201
+ del mock_sleep
202
+ mock_check_output.side_effect = subprocess.CalledProcessError(
203
+ returncode=1, cmd='blah')
204
+ adb_controller = adb_controller_lib.AdbController(
205
+ config_classes.AdbControllerConfig(
206
+ adb_path='my_adb',
207
+ device_name='awesome_device',
208
+ adb_server_port=9999,
209
+ )
210
+ )
211
+ self.assertRaises(
212
+ errors.AdbControllerError,
213
+ adb_controller.execute_command, ['my_command'], timeout=_TIMEOUT)
214
+
47
215
 
48
216
  class AdbControllerInitTest(absltest.TestCase):
49
217
 
50
218
  def test_deletes_problem_env_vars(self):
51
219
  os.environ['ANDROID_HOME'] = '/usr/local/Android/Sdk'
52
220
  os.environ['ANDROID_ADB_SERVER_PORT'] = '1337'
53
- adb_controller.AdbController(
54
- adb_path='my_adb',
55
- device_name='awesome_device',
56
- adb_server_port=9999,
57
- default_timeout=_TIMEOUT)
221
+ adb_controller_lib.AdbController(
222
+ config_classes.AdbControllerConfig(
223
+ adb_path='my_adb',
224
+ device_name='awesome_device',
225
+ adb_server_port=9999,
226
+ default_timeout=_TIMEOUT,
227
+ )
228
+ )
58
229
  self.assertNotIn('ANDROID_HOME', os.environ)
59
230
  self.assertNotIn('ANDROID_ADB_SERVER_PORT', os.environ)
60
231
 
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
16
16
  """Class for a stream of logs output by a locally running emulator."""
17
17
 
18
18
  import subprocess
19
- from typing import List
20
19
 
20
+ from absl import logging
21
21
  from android_env.components import log_stream
22
22
 
23
23
 
@@ -27,11 +27,19 @@ _LOGCAT_COMMAND = ['logcat', '-v', 'epoch']
27
27
  class AdbLogStream(log_stream.LogStream):
28
28
  """Manages adb logcat process for a locally running emulator."""
29
29
 
30
- def __init__(self, adb_command_prefix: List[str], *args, **kwargs):
31
- super().__init__(*args, **kwargs)
30
+ def __init__(self, adb_command_prefix: list[str], verbose: bool = False):
31
+ super().__init__(verbose=verbose)
32
32
  self._adb_command_prefix = adb_command_prefix
33
33
 
34
34
  def _get_stream_output(self):
35
+
36
+ # Before spawning a long-lived process, we issue `logcat -b all -c` to clear
37
+ # all buffers to avoid interference from previous runs.
38
+ clear_buffer_output = subprocess.check_output(
39
+ self._adb_command_prefix + ['logcat', '-b', 'all', '-c'],
40
+ stderr=subprocess.STDOUT,
41
+ timeout=100)
42
+ logging.info('clear_buffer_output: %r', clear_buffer_output)
35
43
  cmd = self._adb_command_prefix + _LOGCAT_COMMAND + self._filters
36
44
  self._adb_subprocess = subprocess.Popen(
37
45
  cmd,
@@ -42,4 +50,8 @@ class AdbLogStream(log_stream.LogStream):
42
50
  return self._adb_subprocess.stdout
43
51
 
44
52
  def stop_stream(self):
45
- self._adb_subprocess.kill()
53
+ if not hasattr(self, '_adb_subprocess') or self._adb_subprocess is None:
54
+ logging.error('`stop_stream()` called before `get_stream_output()`. '
55
+ 'This violates the `LogStream` API.')
56
+ else:
57
+ self._adb_subprocess.kill()
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ from absl.testing import absltest
22
22
  from android_env.components import adb_log_stream
23
23
 
24
24
 
25
- class FakeAdbSubprocess():
25
+ class FakeAdbSubprocess:
26
26
 
27
27
  @property
28
28
  def stdout(self):
@@ -34,8 +34,9 @@ class FakeAdbSubprocess():
34
34
 
35
35
  class AdbLogStreamTest(absltest.TestCase):
36
36
 
37
+ @mock.patch.object(subprocess, 'check_output', return_value=b'')
37
38
  @mock.patch.object(subprocess, 'Popen', return_value=FakeAdbSubprocess())
38
- def test_get_stream_output(self, mock_popen):
39
+ def test_get_stream_output(self, mock_popen, unused_mock_check_output):
39
40
  stream = adb_log_stream.AdbLogStream(adb_command_prefix=['foo'])
40
41
  stream.set_log_filters(['bar'])
41
42
  stream_output = stream.get_stream_output()
@@ -50,6 +51,19 @@ class AdbLogStreamTest(absltest.TestCase):
50
51
  bufsize=1,
51
52
  universal_newlines=True)
52
53
 
54
+ def test_stop_stream_before_get_stream_output(self):
55
+ """Calling `stop_stream()` before `get_stream_output()` should not crash."""
56
+
57
+ # Arrange.
58
+ stream = adb_log_stream.AdbLogStream(adb_command_prefix=['foo'])
59
+
60
+ # Act.
61
+ stream.stop_stream()
62
+
63
+ # Assert.
64
+ # Nothing to assert. The test should just finish without raising an
65
+ # exception.
66
+
53
67
 
54
68
  if __name__ == '__main__':
55
69
  absltest.main()
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -15,23 +15,23 @@
15
15
 
16
16
  """Determines if the current app screen matches an expected app screen."""
17
17
 
18
+ from collections.abc import Callable, Sequence
18
19
  import enum
19
20
  import re
20
21
  import time
21
- from typing import Callable, List, Optional, Sequence, Pattern
22
+ from typing import Self
22
23
 
23
24
  from absl import logging
24
-
25
25
  from android_env.components import adb_call_parser as adb_call_parser_lib
26
26
  from android_env.components import errors
27
27
  from android_env.proto import adb_pb2
28
28
  from android_env.proto import task_pb2
29
29
 
30
30
 
31
- class _DumpsysNode():
31
+ class _DumpsysNode:
32
32
  """A node in a dumpsys tree."""
33
33
 
34
- def __init__(self, data: Optional[str] = None):
34
+ def __init__(self, data: str):
35
35
  self._children = []
36
36
  self._data = data
37
37
 
@@ -40,12 +40,12 @@ class _DumpsysNode():
40
40
  return self._data
41
41
 
42
42
  @property
43
- def children(self) -> List['_DumpsysNode']:
43
+ def children(self) -> list[Self]:
44
44
  return self._children
45
45
 
46
- def find_child(self,
47
- predicate: Callable[['_DumpsysNode'], bool],
48
- max_levels: int = 0) -> Optional['_DumpsysNode']:
46
+ def find_child(
47
+ self, predicate: Callable[[Self], bool], max_levels: int = 0
48
+ ) -> Self | None:
49
49
  """Returns the first direct child that matches `predicate`, None otherwise.
50
50
 
51
51
  Args:
@@ -126,9 +126,11 @@ def build_tree_from_dumpsys_output(dumpsys_output: str) -> _DumpsysNode:
126
126
  return root
127
127
 
128
128
 
129
- def matches_path(dumpsys_activity_output: str,
130
- expected_view_hierarchy_path: Sequence[Pattern[str]],
131
- max_levels: int = 0) -> bool:
129
+ def matches_path(
130
+ dumpsys_activity_output: str,
131
+ expected_view_hierarchy_path: Sequence[re.Pattern[str]],
132
+ max_levels: int = 0,
133
+ ) -> bool:
132
134
  """Returns True if the current dumpsys output matches the expected path.
133
135
 
134
136
  Args:
@@ -150,7 +152,7 @@ def matches_path(dumpsys_activity_output: str,
150
152
  'view_hierarchy is None. Dumpsys activity output: %s. tree: %r',
151
153
  str(dumpsys_activity_output), root.print_tree())
152
154
  logging.error('Tree root: %s', str(root))
153
- return None
155
+ return False
154
156
 
155
157
  current_node = view_hierarchy
156
158
  for i, regex in enumerate(expected_view_hierarchy_path):
@@ -165,13 +167,13 @@ def matches_path(dumpsys_activity_output: str,
165
167
  regex.pattern, current_node)
166
168
  logging.error('Dumpsys activity output: %s', str(dumpsys_activity_output))
167
169
  logging.error('Tree root: %s', str(root))
168
- return None
170
+ return False
169
171
  else:
170
172
  current_node = child
171
173
  return True
172
174
 
173
175
 
174
- class AppScreenChecker():
176
+ class AppScreenChecker:
175
177
  """Checks that the current app screen matches an expected screen."""
176
178
 
177
179
  class Outcome(enum.IntEnum):
@@ -1,5 +1,5 @@
1
1
  # coding=utf-8
2
- # Copyright 2022 DeepMind Technologies Limited.
2
+ # Copyright 2024 DeepMind Technologies Limited.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
16
16
  """Tests for android_env.components.app_screen_checker."""
17
17
 
18
18
  import re
19
- from typing import Sequence
20
19
  from unittest import mock
21
20
 
22
21
  from absl.testing import absltest
@@ -27,13 +26,13 @@ from android_env.proto import adb_pb2
27
26
  from android_env.proto import task_pb2
28
27
 
29
28
 
30
- def flatten_tree(tree: app_screen_checker._DumpsysNode,
31
- flat_tree: Sequence[str],
32
- indent: int = 2):
29
+ def _flatten_tree(
30
+ tree: app_screen_checker._DumpsysNode, flat_tree: list[str], indent: int = 2
31
+ ):
33
32
  """Appends a list of strings to `flat_tree` from `tree`."""
34
33
  flat_tree.append(' ' * indent + tree.data)
35
34
  for c in tree.children:
36
- flatten_tree(c, flat_tree, indent + 2)
35
+ _flatten_tree(c, flat_tree, indent + 2)
37
36
 
38
37
 
39
38
  class AppScreenCheckerTest(absltest.TestCase):
@@ -66,7 +65,7 @@ Queen Elizabeth II
66
65
  """
67
66
  tree = app_screen_checker.build_tree_from_dumpsys_output(dumpsys_output)
68
67
  flat_tree = []
69
- flatten_tree(tree, flat_tree, indent=2)
68
+ _flatten_tree(tree, flat_tree, indent=2)
70
69
  self.assertEqual(flat_tree, [
71
70
  ' ___root___',
72
71
  ' Queen Elizabeth II',
@@ -116,7 +115,7 @@ Tree2
116
115
  """
117
116
  tree = app_screen_checker.build_tree_from_dumpsys_output(dumpsys_output)
118
117
  flat_tree = []
119
- flatten_tree(tree, flat_tree, indent=2)
118
+ _flatten_tree(tree, flat_tree, indent=2)
120
119
  self.assertEqual(flat_tree, [
121
120
  ' ___root___',
122
121
  ' Tree1',