aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMinghao Li <minghaoli@google.com>2023-05-22 10:35:29 +0800
committerGitHub <noreply@github.com>2023-05-22 10:35:29 +0800
commitda3c204be7ceb8d027a6e83456997804f57f5fd0 (patch)
tree94488d3e32d370e4a381c326dc5fd2783e671129
parentb443e440c112ebc5d47629b7dd905cc4ef3e9977 (diff)
downloadmobly-da3c204be7ceb8d027a6e83456997804f57f5fd0.tar.gz
Support am instrument options by adding a snippet config class (#886)
-rw-r--r--mobly/controllers/android_device.py9
-rw-r--r--mobly/controllers/android_device_lib/services/snippet_management_service.py9
-rw-r--r--mobly/controllers/android_device_lib/snippet_client_v2.py45
-rwxr-xr-xtests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py20
-rw-r--r--tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py40
-rwxr-xr-xtests/mobly/controllers/android_device_test.py18
6 files changed, 126 insertions, 15 deletions
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 54166a5..c093e3c 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -901,7 +901,7 @@ class AndroidDevice:
# So we need to wait for the device to come back before proceeding.
self.adb.wait_for_device(timeout=DEFAULT_TIMEOUT_BOOT_COMPLETION_SECOND)
- def load_snippet(self, name, package):
+ def load_snippet(self, name, package, config=None):
"""Starts the snippet apk with the given package name and connects.
Examples:
@@ -917,6 +917,9 @@ class AndroidDevice:
client. E.g. `name='maps'` attaches the snippet client to
`ad.maps`.
package: string, the package name of the snippet apk to connect to.
+ config: snippet_client_v2.Config, the configuration object for
+ controlling the snippet behaviors. See the docstring of the `Config`
+ class for supported configurations.
Raises:
SnippetError: Illegal load operations are attempted.
@@ -926,7 +929,9 @@ class AndroidDevice:
raise SnippetError(
self,
'Attribute "%s" already exists, please use a different name.' % name)
- self.services.snippets.add_snippet_client(name, package)
+ self.services.snippets.add_snippet_client(
+ name, package, config=config
+ )
def unload_snippet(self, name):
"""Stops a snippet apk.
diff --git a/mobly/controllers/android_device_lib/services/snippet_management_service.py b/mobly/controllers/android_device_lib/services/snippet_management_service.py
index fae60e2..05e8cda 100644
--- a/mobly/controllers/android_device_lib/services/snippet_management_service.py
+++ b/mobly/controllers/android_device_lib/services/snippet_management_service.py
@@ -55,7 +55,7 @@ class SnippetManagementService(base_service.BaseService):
if name in self._snippet_clients:
return self._snippet_clients[name]
- def add_snippet_client(self, name, package):
+ def add_snippet_client(self, name, package, config=None):
"""Adds a snippet client to the management.
Args:
@@ -63,6 +63,9 @@ class SnippetManagementService(base_service.BaseService):
client. E.g. `name='maps'` attaches the snippet client to
`ad.maps`.
package: string, the package name of the snippet apk to connect to.
+ config: snippet_client_v2.Config, the configuration object for
+ controlling the snippet behaviors. See the docstring of the `Config`
+ class for supported configurations.
Raises:
Error, if a duplicated name or package is passed in.
@@ -79,7 +82,9 @@ class SnippetManagementService(base_service.BaseService):
self, 'Snippet package "%s" has already been loaded under name'
' "%s".' % (package, snippet_name))
- client = snippet_client_v2.SnippetClientV2(package=package, ad=self._device)
+ client = snippet_client_v2.SnippetClientV2(
+ package=package, ad=self._device, config=config,
+ )
client.initialize()
self._snippet_clients[name] = client
diff --git a/mobly/controllers/android_device_lib/snippet_client_v2.py b/mobly/controllers/android_device_lib/snippet_client_v2.py
index 3adfde5..f7494c2 100644
--- a/mobly/controllers/android_device_lib/snippet_client_v2.py
+++ b/mobly/controllers/android_device_lib/snippet_client_v2.py
@@ -13,10 +13,12 @@
# limitations under the License.
"""Snippet Client V2 for Interacting with Snippet Server on Android Device."""
+import dataclasses
import enum
import json
import re
import socket
+from typing import Dict
from mobly import utils
from mobly.controllers.android_device_lib import adb
@@ -30,8 +32,8 @@ _INSTRUMENTATION_RUNNER_PACKAGE = 'com.google.android.mobly.snippet.SnippetRunne
# The command template to start the snippet server
_LAUNCH_CMD = (
- '{shell_cmd} am instrument {user} -w -e action start {snippet_package}/'
- f'{_INSTRUMENTATION_RUNNER_PACKAGE}')
+ '{shell_cmd} am instrument {user} -w -e action start {instrument_options} '
+ f'{{snippet_package}}/{_INSTRUMENTATION_RUNNER_PACKAGE}')
# The command template to stop the snippet server
_STOP_CMD = ('am instrument {user} -w -e action stop {snippet_package}/'
@@ -76,6 +78,23 @@ _SOCKET_READ_TIMEOUT = 60 * 10
_CALLBACK_DEFAULT_TIMEOUT_SEC = 60 * 2
+@dataclasses.dataclass
+class Config:
+ """A configuration class.
+
+ Attributes:
+ am_instrument_options: The Android am instrument options used for
+ controlling the `onCreate` process of the app under test. Note that this
+ should only be used for controlling the app launch process, options for
+ other purposes may not take effect and you should use snippet RPCs. This
+ is because Mobly snippet runner changes the subsequent instrumentation
+ process.
+ """
+
+ am_instrument_options: Dict[str, str] = dataclasses.field(
+ default_factory=dict)
+
+
class ConnectionHandshakeCommand(enum.Enum):
"""Commands to send to the server when sending the handshake request.
@@ -109,12 +128,14 @@ class SnippetClientV2(client_base.ClientBase):
the connection to the server is made successfully.
"""
- def __init__(self, package, ad):
+ def __init__(self, package, ad, config=None):
"""Initializes the instance of Snippet Client V2.
Args:
package: str, see base class.
ad: AndroidDevice, the android device object associated with this client.
+ config: Config, the configuration object. See the docstring of the
+ `Config` class for supported configurations.
"""
super().__init__(package=package, device=ad)
self.host_port = None
@@ -126,6 +147,7 @@ class SnippetClientV2(client_base.ClientBase):
self._client = None # keep it to prevent close errors on connect failure
self._conn = None
self._event_client = None
+ self._config = config or Config()
@property
def user_id(self):
@@ -231,9 +253,11 @@ class SnippetClientV2(client_base.ClientBase):
self.log.debug('Snippet server for package %s is using protocol %d.%d',
self.package, _PROTOCOL_MAJOR_VERSION,
_PROTOCOL_MINOR_VERSION)
+ option_str = self._get_instrument_options_str()
cmd = _LAUNCH_CMD.format(shell_cmd=persists_shell_cmd,
user=self._get_user_command_string(),
- snippet_package=self.package)
+ snippet_package=self.package,
+ instrument_options=option_str)
self._proc = self._run_adb_cmd(cmd)
# Check protocol version and get the device port
@@ -272,6 +296,19 @@ class SnippetClientV2(client_base.ClientBase):
_SETSID_COMMAND, _NOHUP_COMMAND)
return ''
+ def _get_instrument_options_str(self):
+ self.log.debug(
+ 'Got am instrument options in snippet client for package %s: %s',
+ self.package,
+ self._config.am_instrument_options,
+ )
+ if not self._config.am_instrument_options:
+ return ''
+
+ return ' '.join(
+ f'-e {k} {v}' for k, v in self._config.am_instrument_options.items()
+ )
+
def _get_user_command_string(self):
"""Gets the appropriate command argument for specifying device user ID.
diff --git a/tests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py b/tests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py
index 162847b..16a30aa 100755
--- a/tests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py
+++ b/tests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py
@@ -15,6 +15,7 @@
import unittest
from unittest import mock
+from mobly.controllers.android_device_lib import snippet_client_v2
from mobly.controllers.android_device_lib.services import snippet_management_service
MOCK_PACKAGE = 'com.mock.package'
@@ -63,6 +64,25 @@ class SnippetManagementServiceTest(unittest.TestCase):
mock_client.stop.assert_not_called()
@mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
+ def test_add_snippet_client_without_config(self, mock_class):
+ mock_client = mock_class.return_value
+ manager = snippet_management_service.SnippetManagementService(
+ mock.MagicMock())
+ manager.add_snippet_client('foo', MOCK_PACKAGE)
+ mock_class.assert_called_once_with(
+ package=mock.ANY, ad=mock.ANY, config=None)
+
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
+ def test_add_snippet_client_with_config(self, mock_class):
+ mock_client = mock_class.return_value
+ manager = snippet_management_service.SnippetManagementService(
+ mock.MagicMock())
+ snippet_config = snippet_client_v2.Config()
+ manager.add_snippet_client('foo', MOCK_PACKAGE, snippet_config)
+ mock_class.assert_called_once_with(
+ package=mock.ANY, ad=mock.ANY, config=snippet_config)
+
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_add_snippet_client_dup_name(self, _):
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
diff --git a/tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py b/tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py
index 1943abb..86d889b 100644
--- a/tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py
+++ b/tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py
@@ -92,7 +92,9 @@ def _setup_mock_socket_file(mock_socket_create_conn, resp):
class SnippetClientV2Test(unittest.TestCase):
"""Unit tests for SnippetClientV2."""
- def _make_client(self, adb_proxy=None, mock_properties=None):
+ def _make_client(
+ self, adb_proxy=None, mock_properties=None, config=None
+ ):
adb_proxy = adb_proxy or _MockAdbProxy(instrumented_packages=[
(MOCK_PACKAGE_NAME, snippet_client_v2._INSTRUMENTATION_RUNNER_PACKAGE,
MOCK_PACKAGE_NAME)
@@ -111,7 +113,9 @@ class SnippetClientV2Test(unittest.TestCase):
}
self.device = device
- self.client = snippet_client_v2.SnippetClientV2(MOCK_PACKAGE_NAME, device)
+ self.client = snippet_client_v2.SnippetClientV2(
+ MOCK_PACKAGE_NAME, device, config
+ )
def _make_client_with_extra_adb_properties(self, extra_properties):
mock_properties = mock_android_device.DEFAULT_MOCK_PROPERTIES.copy()
@@ -408,7 +412,7 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
start_cmd_list = [
'adb', 'shell',
- (f'setsid am instrument --user {MOCK_USER_ID} -w -e action start '
+ (f'setsid am instrument --user {MOCK_USER_ID} -w -e action start '
f'{MOCK_SERVER_PATH}')
]
self.assertListEqual(mock_start_subprocess.call_args_list,
@@ -427,7 +431,7 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
start_cmd_list = [
'adb', 'shell',
- f'setsid am instrument -w -e action start {MOCK_SERVER_PATH}'
+ f'setsid am instrument -w -e action start {MOCK_SERVER_PATH}'
]
self.assertListEqual(mock_start_subprocess.call_args_list,
[mock.call(start_cmd_list, shell=False)])
@@ -449,7 +453,7 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
start_cmd_list = [
'adb', 'shell',
- (f' am instrument --user {MOCK_USER_ID} -w -e action start '
+ (f' am instrument --user {MOCK_USER_ID} -w -e action start '
f'{MOCK_SERVER_PATH}')
]
self.assertListEqual(mock_start_subprocess.call_args_list,
@@ -476,7 +480,7 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
start_cmd_list = [
'adb', 'shell',
- (f'nohup am instrument --user {MOCK_USER_ID} -w -e action start '
+ (f'nohup am instrument --user {MOCK_USER_ID} -w -e action start '
f'{MOCK_SERVER_PATH}')
]
self.assertListEqual(mock_start_subprocess.call_args_list,
@@ -499,7 +503,7 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
start_cmd_list = [
'adb', 'shell',
- (f'setsid am instrument --user {MOCK_USER_ID} -w -e action start '
+ (f'setsid am instrument --user {MOCK_USER_ID} -w -e action start '
f'{MOCK_SERVER_PATH}')
]
self.assertListEqual(mock_start_subprocess.call_args_list,
@@ -508,6 +512,28 @@ class SnippetClientV2Test(unittest.TestCase):
@mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
'utils.start_standing_subprocess')
+ def test_start_server_with_instrument_options(self, mock_start_subprocess):
+ """Checks the starting server command with instrument options."""
+ config = snippet_client_v2.Config(
+ am_instrument_options={'key_1': 'val_1', 'key_2': 'val_2'},
+ )
+ instrument_options_str = '-e key_1 val_1 -e key_2 val_2'
+ self._make_client(config=config)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.start_server()
+
+ start_cmd_list = [
+ 'adb', 'shell',
+ (f' am instrument --user {MOCK_USER_ID} -w -e action start '
+ f'{instrument_options_str} {MOCK_SERVER_PATH}')
+ ]
+ self.assertListEqual(mock_start_subprocess.call_args_list,
+ [mock.call(start_cmd_list, shell=False)])
+ self.assertEqual(self.client.device_port, 1234)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
def test_start_server_server_crash(self, mock_start_standing_subprocess):
"""Tests that starting server process crashes."""
self._make_client()
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index fc1fc76..3682c94 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -24,6 +24,7 @@ from mobly import runtime_test_info
from mobly.controllers import android_device
from mobly.controllers.android_device_lib import adb
from mobly.controllers.android_device_lib import errors
+from mobly.controllers.android_device_lib import snippet_client_v2
from mobly.controllers.android_device_lib.services import base_service
from mobly.controllers.android_device_lib.services import logcat
from tests.lib import mock_android_device
@@ -1123,6 +1124,23 @@ class AndroidDeviceTest(unittest.TestCase):
@mock.patch(
'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
+ def test_AndroidDevice_load_snippet_with_snippet_config(
+ self, MockGetPort, MockSnippetClient, MockFastboot, MockAdbProxy):
+ ad = android_device.AndroidDevice(serial='1')
+ snippet_config = snippet_client_v2.Config()
+ ad.load_snippet('snippet', MOCK_SNIPPET_PACKAGE_NAME, snippet_config)
+ self.assertTrue(hasattr(ad, 'snippet'))
+ MockSnippetClient.assert_called_once_with(
+ package=mock.ANY, ad=mock.ANY, config=snippet_config
+ )
+
+ @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+ return_value=mock_android_device.MockAdbProxy('1'))
+ @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+ return_value=mock_android_device.MockFastbootProxy('1'))
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
+ @mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_unload_snippet(self, MockGetPort, MockSnippetClient,
MockFastboot, MockAdbProxy):
ad = android_device.AndroidDevice(serial='1')