blob: 2ed251b9959ddc2bc95d7ccf90f63e2cd36a8ebd [file] [log] [blame]
# 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()