diff --git a/docs/mobly.controllers.android_device_lib.rst b/docs/mobly.controllers.android_device_lib.rst index 8f1d3790..ef3bf0c4 100644 --- a/docs/mobly.controllers.android_device_lib.rst +++ b/docs/mobly.controllers.android_device_lib.rst @@ -83,14 +83,6 @@ mobly.controllers.android\_device\_lib.sl4a\_client module :undoc-members: :show-inheritance: -mobly.controllers.android\_device\_lib.snippet\_client module -------------------------------------------------------------- - -.. automodule:: mobly.controllers.android_device_lib.snippet_client - :members: - :undoc-members: - :show-inheritance: - mobly.controllers.android\_device\_lib.snippet\_event module ------------------------------------------------------------ diff --git a/mobly/controllers/android_device_lib/snippet_client.py b/mobly/controllers/android_device_lib/snippet_client.py deleted file mode 100644 index fed419b4..00000000 --- a/mobly/controllers/android_device_lib/snippet_client.py +++ /dev/null @@ -1,418 +0,0 @@ -# Copyright 2016 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""JSON RPC interface to Mobly Snippet Lib.""" - -import logging -import re -import time - -from mobly import utils -from mobly.controllers.android_device_lib import adb -from mobly.controllers.android_device_lib import errors -from mobly.controllers.android_device_lib import jsonrpc_client_base -from mobly.snippet import errors as snippet_errors - -logging.warning( - 'The module mobly.controllers.android_device_lib.snippet_client' - ' is deprecated and will be removed in a future version. Use' - ' module mobly.controllers.android_device_lib.snippet_client_v2' - ' instead.' -) - -_INSTRUMENTATION_RUNNER_PACKAGE = ( - 'com.google.android.mobly.snippet.SnippetRunner' -) - -# Major version of the launch and communication protocol being used by this -# client. -# Incrementing this means that compatibility with clients using the older -# version is broken. Avoid breaking compatibility unless there is no other -# choice. -_PROTOCOL_MAJOR_VERSION = 1 - -# Minor version of the launch and communication protocol. -# Increment this when new features are added to the launch and communication -# protocol that are backwards compatible with the old protocol and don't break -# existing clients. -_PROTOCOL_MINOR_VERSION = 0 - -_LAUNCH_CMD = ( - '{shell_cmd} am instrument {user} -w -e action start {snippet_package}/' - + _INSTRUMENTATION_RUNNER_PACKAGE -) - -_STOP_CMD = ( - 'am instrument {user} -w -e action stop {snippet_package}/' - + _INSTRUMENTATION_RUNNER_PACKAGE -) - -# Test that uses UiAutomation requires the shell session to be maintained while -# test is in progress. However, this requirement does not hold for the test that -# deals with device USB disconnection (Once device disconnects, the shell -# session that started the instrument ends, and UiAutomation fails with error: -# "UiAutomation not connected"). To keep the shell session and redirect -# stdin/stdout/stderr, use "setsid" or "nohup" while launching the -# instrumentation test. Because these commands may not be available in every -# android system, try to use them only if exists. -_SETSID_COMMAND = 'setsid' - -_NOHUP_COMMAND = 'nohup' - -# Aliases of error types for backward compatibility. -AppStartPreCheckError = snippet_errors.ServerStartPreCheckError -ProtocolVersionError = snippet_errors.ServerStartProtocolError - - -class SnippetClient(jsonrpc_client_base.JsonRpcClientBase): - """A client for interacting with snippet APKs using Mobly Snippet Lib. - - .. deprecated:: 1.13 - Use :class:`mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2` instead. - - See superclass documentation for a list of public attributes. - - For a description of the launch protocols, see the documentation in - mobly-snippet-lib, SnippetRunner.java. - """ - - def __init__(self, package, ad): - """Initializes a SnippetClient. - - Args: - package: (str) The package name of the apk where the snippets are - defined. - ad: (AndroidDevice) the device object associated with this client. - """ - super().__init__(app_name=package, ad=ad) - self.package = package - self._ad = ad - self._adb = ad.adb - self._proc = None - self._user_id = None - - @property - def is_alive(self): - """Does the client have an active connection to the snippet server.""" - return self._conn is not None - - @property - def user_id(self): - """The user id to use for this snippet client. - - This value is cached and, once set, does not change through the lifecycles - of this snippet client object. This caching also reduces the number of adb - calls needed. - - Because all the operations of the snippet client should be done for a - partucular user. - """ - if self._user_id is None: - self._user_id = self._adb.current_user_id - return self._user_id - - def _get_user_command_string(self): - """Gets the appropriate command argument for specifying user IDs. - - By default, `SnippetClient` operates within the current user. - - We don't add the `--user {ID}` arg when Android's SDK is below 24, - where multi-user support is not well implemented. - - Returns: - String, the command param section to be formatted into the adb - commands. - """ - sdk_int = int(self._ad.build_info['build_version_sdk']) - if sdk_int < 24: - return '' - return f'--user {self.user_id}' - - def start_app_and_connect(self): - """Starts snippet apk on the device and connects to it. - - This wraps the main logic with safe handling - - Raises: - AppStartPreCheckError, when pre-launch checks fail. - """ - try: - self._start_app_and_connect() - except AppStartPreCheckError: - # Precheck errors don't need cleanup, directly raise. - raise - except Exception as e: - # Log the stacktrace of `e` as re-raising doesn't preserve trace. - self._ad.log.exception('Failed to start app and connect.') - # If errors happen, make sure we clean up before raising. - try: - self.stop_app() - except Exception: - self._ad.log.exception( - 'Failed to stop app after failure to start and connect.' - ) - # Explicitly raise the original error from starting app. - raise e - - def _start_app_and_connect(self): - """Starts snippet apk on the device and connects to it. - - After prechecks, this launches the snippet apk with an adb cmd in a - standing subprocess, checks the cmd response from the apk for protocol - version, then sets up the socket connection over adb port-forwarding. - - Args: - ProtocolVersionError, if protocol info or port info cannot be - retrieved from the snippet apk. - """ - self._check_app_installed() - self.disable_hidden_api_blacklist() - - persists_shell_cmd = self._get_persist_command() - # Use info here so people can follow along with the snippet startup - # process. Starting snippets can be slow, especially if there are - # multiple, and this avoids the perception that the framework is hanging - # for a long time doing nothing. - self.log.info( - 'Launching snippet apk %s with protocol %d.%d', - self.package, - _PROTOCOL_MAJOR_VERSION, - _PROTOCOL_MINOR_VERSION, - ) - cmd = _LAUNCH_CMD.format( - shell_cmd=persists_shell_cmd, - user=self._get_user_command_string(), - snippet_package=self.package, - ) - start_time = time.perf_counter() - self._proc = self._do_start_app(cmd) - - # Check protocol version and get the device port - line = self._read_protocol_line() - match = re.match('^SNIPPET START, PROTOCOL ([0-9]+) ([0-9]+)$', line) - if not match or match.group(1) != '1': - raise ProtocolVersionError(self._ad, line) - - line = self._read_protocol_line() - match = re.match('^SNIPPET SERVING, PORT ([0-9]+)$', line) - if not match: - raise ProtocolVersionError(self._ad, line) - self.device_port = int(match.group(1)) - - # Forward the device port to a new host port, and connect to that port - self.host_port = utils.get_available_host_port() - self._adb.forward(['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port]) - self.connect() - - # Yaaay! We're done! - self.log.debug( - 'Snippet %s started after %.1fs on host port %s', - self.package, - time.perf_counter() - start_time, - self.host_port, - ) - - def restore_app_connection(self, port=None): - """Restores the app after device got reconnected. - - Instead of creating new instance of the client: - - Uses the given port (or find a new available host_port if none is - given). - - Tries to connect to remote server with selected port. - - Args: - port: If given, this is the host port from which to connect to remote - device port. If not provided, find a new available port as host - port. - - Raises: - AppRestoreConnectionError: When the app was not able to be started. - """ - self.host_port = port or utils.get_available_host_port() - self._adb.forward(['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port]) - try: - self.connect() - except Exception: - # Log the original error and raise AppRestoreConnectionError. - self.log.exception('Failed to re-connect to app.') - raise jsonrpc_client_base.AppRestoreConnectionError( - self._ad, - ( - 'Failed to restore app connection for %s at host port %s, ' - 'device port %s' - ) - % (self.package, self.host_port, self.device_port), - ) - - # Because the previous connection was lost, update self._proc - self._proc = None - self._restore_event_client() - - def stop_app(self): - # Kill the pending 'adb shell am instrument -w' process if there is one. - # Although killing the snippet apk would abort this process anyway, we - # want to call stop_standing_subprocess() to perform a health check, - # print the failure stack trace if there was any, and reap it from the - # process table. - self.log.debug('Stopping snippet apk %s', self.package) - # Close the socket connection. - self.disconnect() - if self._proc: - utils.stop_standing_subprocess(self._proc) - self._proc = None - out = self._adb.shell( - _STOP_CMD.format( - snippet_package=self.package, user=self._get_user_command_string() - ) - ).decode('utf-8') - if 'OK (0 tests)' not in out: - raise errors.DeviceError( - self._ad, 'Failed to stop existing apk. Unexpected output: %s' % out - ) - - self._stop_event_client() - - def _start_event_client(self): - """Overrides superclass.""" - event_client = SnippetClient(package=self.package, ad=self._ad) - event_client.host_port = self.host_port - event_client.device_port = self.device_port - event_client.connect(self.uid, jsonrpc_client_base.JsonRpcCommand.CONTINUE) - return event_client - - def _stop_event_client(self): - """Releases all the resources acquired in `_start_event_client`.""" - if self._event_client: - self._event_client.close_socket_connection() - # Without cleaning host_port of event_client, the event client will try to - # stop the port forwarding when deconstructed, which should only be - # stopped by the corresponding snippet client. - self._event_client.host_port = None - self._event_client.device_port = None - self._event_client = None - - def _restore_event_client(self): - """Restores previously created event client.""" - if not self._event_client: - self._event_client = self._start_event_client() - return - self._event_client.host_port = self.host_port - self._event_client.device_port = self.device_port - self._event_client.connect() - - def _check_app_installed(self): - # Check that the Mobly Snippet app is installed for the current user. - out = self._adb.shell(f'pm list package --user {self.user_id}') - if not utils.grep('^package:%s$' % self.package, out): - raise AppStartPreCheckError( - self._ad, f'{self.package} is not installed for user {self.user_id}.' - ) - # Check that the app is instrumented. - out = self._adb.shell('pm list instrumentation') - matched_out = utils.grep( - f'^instrumentation:{self.package}/{_INSTRUMENTATION_RUNNER_PACKAGE}', - out, - ) - if not matched_out: - raise AppStartPreCheckError( - self._ad, f'{self.package} is installed, but it is not instrumented.' - ) - match = re.search( - r'^instrumentation:(.*)\/(.*) \(target=(.*)\)$', matched_out[0] - ) - target_name = match.group(3) - # Check that the instrumentation target is installed if it's not the - # same as the snippet package. - if target_name != self.package: - out = self._adb.shell(f'pm list package --user {self.user_id}') - if not utils.grep('^package:%s$' % target_name, out): - raise AppStartPreCheckError( - self._ad, - f'Instrumentation target {target_name} is not installed for user ' - f'{self.user_id}.', - ) - - def _do_start_app(self, launch_cmd): - adb_cmd = [adb.ADB] - if self._adb.serial: - adb_cmd += ['-s', self._adb.serial] - adb_cmd += ['shell', launch_cmd] - return utils.start_standing_subprocess(adb_cmd, shell=False) - - def _read_protocol_line(self): - """Reads the next line of instrumentation output relevant to snippets. - - This method will skip over lines that don't start with 'SNIPPET' or - 'INSTRUMENTATION_RESULT'. - - Returns: - (str) Next line of snippet-related instrumentation output, stripped. - - Raises: - jsonrpc_client_base.AppStartError: If EOF is reached without any - protocol lines being read. - """ - while True: - line = self._proc.stdout.readline().decode('utf-8') - if not line: - raise jsonrpc_client_base.AppStartError( - self._ad, 'Unexpected EOF waiting for app to start' - ) - # readline() uses an empty string to mark EOF, and a single newline - # to mark regular empty lines in the output. Don't move the strip() - # call above the truthiness check, or this method will start - # considering any blank output line to be EOF. - line = line.strip() - if line.startswith('INSTRUMENTATION_RESULT:') or line.startswith( - 'SNIPPET ' - ): - self.log.debug('Accepted line from instrumentation output: "%s"', line) - return line - self.log.debug('Discarded line from instrumentation output: "%s"', line) - - def _get_persist_command(self): - """Check availability and return path of command if available.""" - for command in [_SETSID_COMMAND, _NOHUP_COMMAND]: - try: - if command in self._adb.shell(['which', command]).decode('utf-8'): - return command - except adb.AdbError: - continue - self.log.warning( - 'No %s and %s commands available to launch instrument ' - 'persistently, tests that depend on UiAutomator and ' - 'at the same time performs USB disconnection may fail', - _SETSID_COMMAND, - _NOHUP_COMMAND, - ) - return '' - - def help(self, print_output=True): - """Calls the help RPC, which returns the list of RPC calls available. - - This RPC should normally be used in an interactive console environment - where the output should be printed instead of returned. Otherwise, - newlines will be escaped, which will make the output difficult to read. - - Args: - print_output: A bool for whether the output should be printed. - - Returns: - A str containing the help output otherwise None if print_output - wasn't set. - """ - help_text = self._rpc('help') - if print_output: - print(help_text) - else: - return help_text diff --git a/tests/mobly/controllers/android_device_lib/snippet_client_test.py b/tests/mobly/controllers/android_device_lib/snippet_client_test.py deleted file mode 100755 index 2ed251b9..00000000 --- a/tests/mobly/controllers/android_device_lib/snippet_client_test.py +++ /dev/null @@ -1,719 +0,0 @@ -# Copyright 2017 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import unittest -from unittest import mock - -from mobly.controllers.android_device_lib import adb -from mobly.controllers.android_device_lib import jsonrpc_client_base -from mobly.controllers.android_device_lib import snippet_client -from tests.lib import jsonrpc_client_test_base -from tests.lib import mock_android_device - -MOCK_PACKAGE_NAME = 'some.package.name' -MOCK_MISSING_PACKAGE_NAME = 'not.installed' -JSONRPC_BASE_CLASS = ( - 'mobly.controllers.android_device_lib.jsonrpc_client_base.JsonRpcClientBase' -) -MOCK_USER_ID = 0 - - -class SnippetClientTest(jsonrpc_client_test_base.JsonRpcClientTestBase): - """Unit tests for mobly.controllers.android_device_lib.snippet_client.""" - - def test_check_app_installed_normal(self): - sc = self._make_client() - sc._check_app_installed() - - def test_check_app_installed_fail_app_not_installed(self): - sc = self._make_client(mock_android_device.MockAdbProxy()) - expected_msg = '.* %s is not installed.' % MOCK_PACKAGE_NAME - with self.assertRaisesRegex( - snippet_client.AppStartPreCheckError, expected_msg - ): - sc._check_app_installed() - - def test_check_app_installed_fail_not_instrumented(self): - sc = self._make_client( - mock_android_device.MockAdbProxy(installed_packages=[MOCK_PACKAGE_NAME]) - ) - expected_msg = ( - '.* %s is installed, but it is not instrumented.' % MOCK_PACKAGE_NAME - ) - with self.assertRaisesRegex( - snippet_client.AppStartPreCheckError, expected_msg - ): - sc._check_app_installed() - - def test_check_app_installed_fail_target_not_installed(self): - sc = self._make_client( - mock_android_device.MockAdbProxy( - instrumented_packages=[ - ( - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - MOCK_MISSING_PACKAGE_NAME, - ) - ] - ) - ) - expected_msg = ( - '.* Instrumentation target %s is not installed.' - % MOCK_MISSING_PACKAGE_NAME - ) - with self.assertRaisesRegex( - snippet_client.AppStartPreCheckError, expected_msg - ): - sc._check_app_installed() - - @mock.patch('socket.create_connection') - def test_snippet_start(self, mock_create_connection): - self.setup_mock_socket_file(mock_create_connection) - client = self._make_client() - client.connect() - result = client.testSnippetCall() - self.assertEqual(123, result) - - @mock.patch('socket.create_connection') - def test_snippet_start_event_client(self, mock_create_connection): - fake_file = self.setup_mock_socket_file(mock_create_connection) - client = self._make_client() - client.host_port = 123 # normally picked by start_app_and_connect - client.connect() - fake_file.resp = self.MOCK_RESP_WITH_CALLBACK - callback = client.testSnippetCall() - self.assertEqual(123, callback.ret_value) - self.assertEqual('1-0', callback._id) - - # Check to make sure the event client is using the same port as the - # main client. - self.assertEqual(123, callback._event_client.host_port) - - fake_file.resp = self.MOCK_RESP_WITH_ERROR - with self.assertRaisesRegex(jsonrpc_client_base.ApiError, '1'): - callback.getAll('eventName') - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_restore_event_client( - self, mock_get_port, mock_create_connection - ): - mock_get_port.return_value = 789 - fake_file = self.setup_mock_socket_file(mock_create_connection) - client = self._make_client() - client.host_port = 123 # normally picked by start_app_and_connect - client.device_port = 456 - client.connect() - fake_file.resp = self.MOCK_RESP_WITH_CALLBACK - callback = client.testSnippetCall() - - # before reconnect, clients use previously selected ports - self.assertEqual(123, client.host_port) - self.assertEqual(456, client.device_port) - self.assertEqual(123, callback._event_client.host_port) - self.assertEqual(456, callback._event_client.device_port) - - # after reconnect, if host port specified, clients use specified port - client.restore_app_connection(port=321) - self.assertEqual(321, client.host_port) - self.assertEqual(456, client.device_port) - self.assertEqual(321, callback._event_client.host_port) - self.assertEqual(456, callback._event_client.device_port) - - # after reconnect, if host port not specified, clients use selected - # available port - client.restore_app_connection() - self.assertEqual(789, client.host_port) - self.assertEqual(456, client.device_port) - self.assertEqual(789, callback._event_client.host_port) - self.assertEqual(456, callback._event_client.device_port) - - # if unable to reconnect for any reason, a - # jsonrpc_client_base.AppRestoreConnectionError is raised. - mock_create_connection.side_effect = IOError('socket timed out') - with self.assertRaisesRegex( - jsonrpc_client_base.AppRestoreConnectionError, - ( - 'Failed to restore app connection for %s at host port %s, ' - 'device port %s' - ) - % (MOCK_PACKAGE_NAME, 789, 456), - ): - client.restore_app_connection() - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_and_connect( - self, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'SNIPPET START, PROTOCOL 1 0\n', - b'SNIPPET SERVING, PORT 123\n', - ], - ) - client = self._make_client() - client.start_app_and_connect() - self.assertEqual(123, client.device_port) - self.assertTrue(client.is_alive) - - @mock.patch('socket.create_connection') - @mock.patch('mobly.utils.stop_standing_subprocess') - def test_snippet_stop_app( - self, mock_stop_standing_subprocess, mock_create_connection - ): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - client.stop_app() - self.assertFalse(client.is_alive) - - def test_snippet_stop_app_raises(self): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - client.host_port = 1 - client._conn = mock.MagicMock() - # Explicitly making the second side_effect noop to avoid uncaught exception - # when `__del__` is called after the test is done, which triggers - # `disconnect`. - client._conn.close.side_effect = [Exception('ha'), None] - with self.assertRaisesRegex(Exception, 'ha'): - client.stop_app() - adb_proxy.forward.assert_called_once_with(['--remove', 'tcp:1']) - - @mock.patch('socket.create_connection') - @mock.patch('mobly.utils.stop_standing_subprocess') - def test_snippet_stop_app_stops_event_client( - self, mock_stop_standing_subprocess, mock_create_connection - ): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - event_client = snippet_client.SnippetClient( - package=MOCK_PACKAGE_NAME, ad=client._ad - ) - client._event_client = event_client - event_client_conn = mock.Mock() - event_client._conn = event_client_conn - - client.stop_app() - self.assertFalse(client.is_alive) - event_client_conn.close.assert_called_once() - self.assertIsNone(client._event_client) - self.assertIsNone(event_client._conn) - - @mock.patch('socket.create_connection') - @mock.patch('mobly.utils.stop_standing_subprocess') - def test_snippet_stop_app_stops_event_client_without_connection( - self, mock_stop_standing_subprocess, mock_create_connection - ): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - event_client = snippet_client.SnippetClient( - package=MOCK_PACKAGE_NAME, ad=client._ad - ) - client._event_client = event_client - event_client._conn = None - - client.stop_app() - self.assertFalse(client.is_alive) - self.assertIsNone(client._event_client) - self.assertIsNone(event_client._conn) - - @mock.patch('socket.create_connection') - @mock.patch('mobly.utils.stop_standing_subprocess') - def test_snippet_stop_app_without_event_client( - self, mock_stop_standing_subprocess, mock_create_connection - ): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - client._event_client = None - - client.stop_app() - self.assertFalse(client.is_alive) - self.assertIsNone(client._event_client) - - @mock.patch('socket.create_connection') - @mock.patch('mobly.utils.stop_standing_subprocess') - @mock.patch.object(snippet_client.SnippetClient, 'connect') - def test_event_client_does_not_stop_port_forwarding( - self, mock_stop_standing_subprocess, mock_create_connection, mock_connect - ): - adb_proxy = mock.MagicMock() - adb_proxy.shell.return_value = b'OK (0 tests)' - client = self._make_client(adb_proxy) - client.host_port = 12345 - client.device_port = 67890 - - event_client = client._start_event_client() - # Mock adb proxy of event client to validate forward call - event_client._ad = mock.MagicMock() - event_client._adb = event_client._ad.adb - client._event_client = event_client - - # Verify that neither the stop process nor the deconstructor is trying to - # stop the port forwarding - client.stop_app() - event_client.__del__() - - event_client._adb.forward.assert_not_called() - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient.' - 'disable_hidden_api_blacklist' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient.' - 'stop_app' - ) - def test_start_app_and_connect_precheck_fail( - self, - mock_stop, - mock_precheck, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'SNIPPET START, PROTOCOL 1 0\n', - b'SNIPPET SERVING, PORT 123\n', - ], - ) - client = self._make_client() - mock_precheck.side_effect = snippet_client.AppStartPreCheckError( - client.ad, 'ha' - ) - with self.assertRaisesRegex(snippet_client.AppStartPreCheckError, 'ha'): - client.start_app_and_connect() - mock_stop.assert_not_called() - self.assertFalse(client.is_alive) - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient._start_app_and_connect' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient.stop_app' - ) - def test_start_app_and_connect_generic_error( - self, - mock_stop, - mock_start, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'SNIPPET START, PROTOCOL 1 0\n', - b'SNIPPET SERVING, PORT 123\n', - ], - ) - client = self._make_client() - mock_start.side_effect = Exception('ha') - with self.assertRaisesRegex(Exception, 'ha'): - client.start_app_and_connect() - mock_stop.assert_called_once_with() - self.assertFalse(client.is_alive) - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient._start_app_and_connect' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.SnippetClient.stop_app' - ) - def test_start_app_and_connect_fail_stop_also_fail( - self, - mock_stop, - mock_start, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'SNIPPET START, PROTOCOL 1 0\n', - b'SNIPPET SERVING, PORT 123\n', - ], - ) - client = self._make_client() - mock_start.side_effect = Exception('Some error') - mock_stop.side_effect = Exception('Another error') - with self.assertRaisesRegex(Exception, 'Some error'): - client.start_app_and_connect() - mock_stop.assert_called_once_with() - self.assertFalse(client.is_alive) - - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._do_start_app' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._check_app_installed' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._read_protocol_line' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient.connect' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_on_sdk_21( - self, - mock_get_port, - mock_connect, - mock_read_protocol_line, - mock_check_app_installed, - mock_do_start_app, - ): - """Check that `--user` is not added to start command on SDK < 24.""" - - def _mocked_shell(arg): - if 'setsid' in arg: - raise adb.AdbError('cmd', 'stdout', 'stderr', 'ret_code') - else: - return b'nohup' - - mock_get_port.return_value = 123 - mock_read_protocol_line.side_effect = [ - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - ] - - # Test 'setsid' exists - client = self._make_client() - client._ad.build_info['build_version_sdk'] = 21 - client._adb.shell = mock.Mock(return_value=b'setsid') - client.start_app_and_connect() - cmd_setsid = '%s am instrument -w -e action start %s/%s' % ( - snippet_client._SETSID_COMMAND, - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - ) - mock_do_start_app.assert_has_calls([mock.call(cmd_setsid)]) - - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._do_start_app' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._check_app_installed' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient._read_protocol_line' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'SnippetClient.connect' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_and_connect_persistent_session( - self, - mock_get_port, - mock_connect, - mock_read_protocol_line, - mock_check_app_installed, - mock_do_start_app, - ): - def _mocked_shell(arg): - if 'setsid' in arg: - raise adb.AdbError('cmd', 'stdout', 'stderr', 'ret_code') - else: - return b'nohup' - - mock_get_port.return_value = 123 - mock_read_protocol_line.side_effect = [ - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - 'SNIPPET START, PROTOCOL 1 234', - 'SNIPPET SERVING, PORT 1234', - ] - - # Test 'setsid' exists - client = self._make_client() - client._adb = mock.MagicMock() - client._adb.shell.return_value = b'setsid' - client._adb.current_user_id = MOCK_USER_ID - client.start_app_and_connect() - cmd_setsid = '%s am instrument --user %s -w -e action start %s/%s' % ( - snippet_client._SETSID_COMMAND, - MOCK_USER_ID, - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - ) - mock_do_start_app.assert_has_calls([mock.call(cmd_setsid)]) - - # Test 'setsid' does not exist, but 'nohup' exsits - client = self._make_client() - client._adb.shell = _mocked_shell - client.start_app_and_connect() - cmd_nohup = '%s am instrument --user %s -w -e action start %s/%s' % ( - snippet_client._NOHUP_COMMAND, - MOCK_USER_ID, - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - ) - mock_do_start_app.assert_has_calls( - [mock.call(cmd_setsid), mock.call(cmd_nohup)] - ) - - # Test both 'setsid' and 'nohup' do not exist - client._adb.shell = mock.Mock( - side_effect=adb.AdbError('cmd', 'stdout', 'stderr', 'ret_code') - ) - client = self._make_client() - client.start_app_and_connect() - cmd_not_persist = ' am instrument --user %s -w -e action start %s/%s' % ( - MOCK_USER_ID, - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - ) - mock_do_start_app.assert_has_calls( - [ - mock.call(cmd_setsid), - mock.call(cmd_nohup), - mock.call(cmd_not_persist), - ] - ) - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_crash( - self, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - mock_get_port.return_value = 456 - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[b'INSTRUMENTATION_RESULT: shortMsg=Process crashed.\n'], - ) - client = self._make_client() - with self.assertRaisesRegex( - snippet_client.ProtocolVersionError, - 'INSTRUMENTATION_RESULT: shortMsg=Process crashed.', - ): - client.start_app_and_connect() - - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_and_connect_unknown_protocol( - self, mock_get_port, mock_start_standing_subprocess - ): - mock_get_port.return_value = 789 - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[b'SNIPPET START, PROTOCOL 99 0\n'], - ) - client = self._make_client() - with self.assertRaisesRegex( - snippet_client.ProtocolVersionError, 'SNIPPET START, PROTOCOL 99 0' - ): - client.start_app_and_connect() - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_and_connect_header_junk( - self, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'This is some header junk\n', - b'Some phones print arbitrary output\n', - b'SNIPPET START, PROTOCOL 1 0\n', - b'Maybe in the middle too\n', - b'SNIPPET SERVING, PORT 123\n', - ], - ) - client = self._make_client() - client.start_app_and_connect() - self.assertEqual(123, client.device_port) - - @mock.patch('socket.create_connection') - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.start_standing_subprocess' - ) - @mock.patch( - 'mobly.controllers.android_device_lib.snippet_client.' - 'utils.get_available_host_port' - ) - def test_snippet_start_app_and_connect_no_valid_line( - self, - mock_get_port, - mock_start_standing_subprocess, - mock_create_connection, - ): - mock_get_port.return_value = 456 - self.setup_mock_socket_file(mock_create_connection) - self._setup_mock_instrumentation_cmd( - mock_start_standing_subprocess, - resp_lines=[ - b'This is some header junk\n', - b'Some phones print arbitrary output\n', - b'', # readline uses '' to mark EOF - ], - ) - client = self._make_client() - with self.assertRaisesRegex( - jsonrpc_client_base.AppStartError, - 'Unexpected EOF waiting for app to start', - ): - client.start_app_and_connect() - - @mock.patch('builtins.print') - def test_help_rpc_when_printing_by_default(self, mock_print): - client = self._make_client() - mock_rpc = mock.MagicMock() - client._rpc = mock_rpc - - result = client.help() - mock_rpc.assert_called_once_with('help') - self.assertEqual(None, result) - mock_print.assert_called_once_with(mock_rpc.return_value) - - @mock.patch('builtins.print') - def test_help_rpc_when_not_printing(self, mock_print): - client = self._make_client() - mock_rpc = mock.MagicMock() - client._rpc = mock_rpc - - result = client.help(print_output=False) - mock_rpc.assert_called_once_with('help') - self.assertEqual(mock_rpc.return_value, result) - mock_print.assert_not_called() - - def _make_client(self, adb_proxy=None): - adb_proxy = adb_proxy or mock_android_device.MockAdbProxy( - instrumented_packages=[ - ( - MOCK_PACKAGE_NAME, - snippet_client._INSTRUMENTATION_RUNNER_PACKAGE, - MOCK_PACKAGE_NAME, - ) - ] - ) - ad = mock.Mock() - ad.adb = adb_proxy - ad.adb.current_user_id = MOCK_USER_ID - ad.build_info = { - 'build_version_codename': ad.adb.getprop('ro.build.version.codename'), - 'build_version_sdk': ad.adb.getprop('ro.build.version.sdk'), - } - return snippet_client.SnippetClient(package=MOCK_PACKAGE_NAME, ad=ad) - - def _setup_mock_instrumentation_cmd( - self, mock_start_standing_subprocess, resp_lines - ): - mock_proc = mock_start_standing_subprocess() - mock_proc.stdout.readline.side_effect = resp_lines - - -if __name__ == '__main__': - unittest.main()