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.
- android_env/__init__.py +1 -1
- android_env/components/__init__.py +1 -1
- android_env/components/a11y/__init__.py +15 -0
- android_env/components/a11y/a11y_events.py +118 -0
- android_env/components/a11y/a11y_events_test.py +173 -0
- android_env/components/a11y/a11y_forests.py +128 -0
- android_env/components/a11y/a11y_forests_test.py +237 -0
- android_env/components/a11y/a11y_servicer.py +199 -0
- android_env/components/a11y/a11y_servicer_test.py +224 -0
- android_env/components/action_fns.py +132 -0
- android_env/components/action_fns_test.py +227 -0
- android_env/components/action_type.py +26 -3
- android_env/components/adb_call_parser.py +239 -196
- android_env/components/adb_call_parser_test.py +179 -209
- android_env/components/adb_controller.py +90 -52
- android_env/components/adb_controller_test.py +187 -16
- android_env/components/adb_log_stream.py +17 -5
- android_env/components/adb_log_stream_test.py +17 -3
- android_env/components/app_screen_checker.py +17 -15
- android_env/components/app_screen_checker_test.py +7 -8
- android_env/components/config_classes.py +203 -0
- android_env/components/coordinator.py +102 -338
- android_env/components/coordinator_test.py +59 -199
- android_env/components/device_settings.py +174 -0
- android_env/components/device_settings_test.py +228 -0
- android_env/components/dumpsys_thread.py +3 -4
- android_env/components/dumpsys_thread_test.py +1 -1
- android_env/components/errors.py +52 -10
- android_env/components/errors_test.py +110 -0
- android_env/components/log_stream.py +7 -5
- android_env/components/log_stream_test.py +1 -1
- android_env/components/logcat_thread.py +9 -8
- android_env/components/logcat_thread_test.py +3 -4
- android_env/components/{utils.py → pixel_fns.py} +20 -20
- android_env/components/{utils_test.py → pixel_fns_test.py} +20 -15
- android_env/components/setup_step_interpreter.py +47 -39
- android_env/components/setup_step_interpreter_test.py +4 -4
- android_env/components/simulators/__init__.py +1 -1
- android_env/components/simulators/base_simulator.py +116 -44
- android_env/components/simulators/base_simulator_test.py +131 -9
- android_env/components/simulators/emulator/__init__.py +1 -1
- android_env/components/simulators/emulator/emulator_launcher.py +67 -77
- android_env/components/simulators/emulator/emulator_launcher_test.py +153 -49
- android_env/components/simulators/emulator/emulator_simulator.py +276 -95
- android_env/components/simulators/emulator/emulator_simulator_test.py +314 -89
- android_env/components/simulators/fake/__init__.py +1 -1
- android_env/components/simulators/fake/fake_simulator.py +17 -25
- android_env/components/simulators/fake/fake_simulator_test.py +29 -12
- android_env/components/specs.py +18 -28
- android_env/components/specs_test.py +1 -44
- android_env/components/task_manager.py +48 -48
- android_env/components/task_manager_test.py +71 -60
- android_env/env_interface.py +37 -23
- android_env/environment.py +83 -51
- android_env/environment_test.py +68 -29
- android_env/loader.py +57 -43
- android_env/loader_test.py +115 -35
- android_env/proto/__init__.py +1 -1
- android_env/proto/a11y/__init__.py +15 -0
- android_env/proto/a11y/a11y.proto +75 -0
- android_env/proto/a11y/a11y_pb2.py +54 -0
- android_env/proto/a11y/a11y_pb2.pyi +49 -0
- android_env/proto/a11y/a11y_pb2_grpc.py +202 -0
- android_env/proto/a11y/android_accessibility_action.proto +32 -0
- android_env/proto/a11y/android_accessibility_action_pb2.py +37 -0
- android_env/proto/a11y/android_accessibility_action_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_action_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_forest.proto +29 -0
- android_env/proto/a11y/android_accessibility_forest_pb2.py +38 -0
- android_env/proto/a11y/android_accessibility_forest_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_forest_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_node_info.proto +122 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span.proto +49 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.py +39 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2.pyi +28 -0
- android_env/proto/a11y/android_accessibility_node_info_clickable_span_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2.py +42 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2.pyi +75 -0
- android_env/proto/a11y/android_accessibility_node_info_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_tree.proto +29 -0
- android_env/proto/a11y/android_accessibility_tree_pb2.py +38 -0
- android_env/proto/a11y/android_accessibility_tree_pb2.pyi +13 -0
- android_env/proto/a11y/android_accessibility_tree_pb2_grpc.py +24 -0
- android_env/proto/a11y/android_accessibility_window_info.proto +84 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2.py +41 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2.pyi +48 -0
- android_env/proto/a11y/android_accessibility_window_info_pb2_grpc.py +24 -0
- android_env/proto/a11y/rect.proto +30 -0
- android_env/proto/a11y/rect_pb2.py +37 -0
- android_env/proto/a11y/rect_pb2.pyi +17 -0
- android_env/proto/a11y/rect_pb2_grpc.py +24 -0
- android_env/proto/adb.proto +17 -6
- android_env/proto/adb_pb2.py +120 -107
- android_env/proto/adb_pb2.pyi +396 -0
- android_env/proto/adb_pb2_grpc.py +20 -0
- android_env/proto/emulator_controller.proto +68 -63
- android_env/proto/emulator_controller_pb2.py +142 -131
- android_env/proto/emulator_controller_pb2.pyi +672 -0
- android_env/proto/emulator_controller_pb2_grpc.py +505 -142
- android_env/proto/snapshot.proto +169 -0
- android_env/proto/snapshot_pb2.py +47 -0
- android_env/proto/snapshot_pb2.pyi +117 -0
- android_env/proto/snapshot_pb2_grpc.py +24 -0
- android_env/proto/snapshot_service.proto +289 -0
- android_env/proto/snapshot_service_pb2.py +54 -0
- android_env/proto/snapshot_service_pb2.pyi +86 -0
- android_env/proto/snapshot_service_pb2_grpc.py +487 -0
- android_env/proto/state.proto +63 -0
- android_env/proto/state_pb2.py +63 -0
- android_env/proto/state_pb2.pyi +85 -0
- android_env/proto/state_pb2_grpc.py +24 -0
- android_env/proto/task.proto +5 -1
- android_env/proto/task_pb2.py +42 -31
- android_env/proto/task_pb2.pyi +160 -0
- android_env/proto/task_pb2_grpc.py +20 -0
- android_env/wrappers/__init__.py +1 -1
- android_env/wrappers/a11y_grpc_wrapper.py +500 -0
- android_env/wrappers/a11y_grpc_wrapper_test.py +849 -0
- android_env/wrappers/base_wrapper.py +34 -13
- android_env/wrappers/base_wrapper_test.py +22 -16
- android_env/wrappers/discrete_action_wrapper.py +18 -17
- android_env/wrappers/discrete_action_wrapper_test.py +4 -4
- android_env/wrappers/flat_interface_wrapper.py +5 -5
- android_env/wrappers/flat_interface_wrapper_test.py +7 -11
- android_env/wrappers/float_pixels_wrapper.py +9 -10
- android_env/wrappers/float_pixels_wrapper_test.py +3 -3
- android_env/wrappers/gym_wrapper.py +19 -13
- android_env/wrappers/gym_wrapper_test.py +3 -5
- android_env/wrappers/image_rescale_wrapper.py +18 -21
- android_env/wrappers/image_rescale_wrapper_test.py +25 -37
- android_env/wrappers/last_action_wrapper.py +16 -13
- android_env/wrappers/last_action_wrapper_test.py +44 -51
- android_env/wrappers/rate_limit_wrapper.py +6 -3
- android_env/wrappers/rate_limit_wrapper_test.py +22 -1
- android_env/wrappers/tap_action_wrapper.py +16 -17
- android_env/wrappers/tap_action_wrapper_test.py +51 -16
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/METADATA +14 -18
- android_env-1.2.3.dist-info/RECORD +141 -0
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info}/WHEEL +1 -1
- android_env/proto/raw_observation.proto +0 -39
- android_env/proto/raw_observation_pb2.py +0 -27
- android_env/proto/raw_observation_pb2_grpc.py +0 -4
- android_env-1.2.1.dist-info/RECORD +0 -81
- {android_env-1.2.1.dist-info → android_env-1.2.3.dist-info/licenses}/LICENSE +0 -0
- {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
|
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
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
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 = [
|
59
|
-
|
60
|
-
|
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:
|
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
|
-
|
75
|
-
|
76
|
-
|
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(
|
82
|
-
|
83
|
-
|
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.
|
96
|
-
command = self.command_prefix() + args
|
97
|
-
command_str = ' '.join(command)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
self.
|
38
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
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:
|
31
|
-
super().__init__(
|
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
|
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
|
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
|
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
|
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:
|
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) ->
|
43
|
+
def children(self) -> list[Self]:
|
44
44
|
return self._children
|
45
45
|
|
46
|
-
def find_child(
|
47
|
-
|
48
|
-
|
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(
|
130
|
-
|
131
|
-
|
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
|
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
|
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
|
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
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
118
|
+
_flatten_tree(tree, flat_tree, indent=2)
|
120
119
|
self.assertEqual(flat_tree, [
|
121
120
|
' ___root___',
|
122
121
|
' Tree1',
|