aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/lib/terminated_test.py38
-rwxr-xr-xtests/mobly/base_test_test.py100
-rwxr-xr-xtests/mobly/controllers/android_device_lib/adb_test.py15
-rwxr-xr-xtests/mobly/controllers/android_device_lib/callback_handler_test.py2
-rw-r--r--tests/mobly/controllers/android_device_lib/callback_handler_v2_test.py152
-rw-r--r--tests/mobly/controllers/android_device_lib/fastboot_test.py43
-rwxr-xr-xtests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py16
-rwxr-xr-xtests/mobly/controllers/android_device_lib/services/snippet_management_service_test.py46
-rwxr-xr-xtests/mobly/controllers/android_device_lib/snippet_client_test.py75
-rw-r--r--tests/mobly/controllers/android_device_lib/snippet_client_v2_test.py1146
-rwxr-xr-xtests/mobly/controllers/android_device_test.py24
-rwxr-xr-xtests/mobly/logger_test.py31
-rwxr-xr-xtests/mobly/records_test.py54
-rwxr-xr-xtests/mobly/snippet/callback_event_test.py40
-rw-r--r--tests/mobly/snippet/callback_handler_base_test.py183
-rwxr-xr-xtests/mobly/suite_runner_test.py48
-rwxr-xr-xtests/mobly/test_runner_test.py20
-rwxr-xr-xtests/mobly/utils_test.py2
18 files changed, 1894 insertions, 141 deletions
diff --git a/tests/lib/terminated_test.py b/tests/lib/terminated_test.py
new file mode 100644
index 0000000..b9dfdf4
--- /dev/null
+++ b/tests/lib/terminated_test.py
@@ -0,0 +1,38 @@
+# Copyright 2022 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 logging
+import os
+import platform
+import signal
+
+from mobly import base_test
+from mobly import signals
+from mobly import test_runner
+
+
+class TerminatedTest(base_test.BaseTestClass):
+
+ def test_terminated(self):
+ # SIGTERM handler does not work on Windows. So just simulate the behaviour
+ # for the purpose of this test.
+ if platform.system() == 'Windows':
+ logging.warning('Test received a SIGTERM. Aborting all tests.')
+ raise signals.TestAbortAll('Test received a SIGTERM.')
+ else:
+ os.kill(os.getpid(), signal.SIGTERM)
+
+
+if __name__ == '__main__':
+ test_runner.main()
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
index ebac0e0..be67437 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -1728,6 +1728,7 @@ class BaseTestTest(unittest.TestCase):
self.assertEqual(actual_record.test_name, 'teardown_class')
self.assertEqual(actual_record.details, MSG_EXPECTED_EXCEPTION)
self.assertEqual(actual_record.extras, MOCK_EXTRA)
+ self.assertIsNotNone(actual_record.end_time)
def test_expect_in_setup_test(self):
must_call = mock.Mock()
@@ -1960,6 +1961,62 @@ class BaseTestTest(unittest.TestCase):
bc.unpack_userparams(arg1="haha")
self.assertEqual(bc.arg1, "haha")
+ def test_pre_run_failure(self):
+ """Test code path for `pre_run` failure.
+
+ When `pre_run` fails, pre-execution calculation is incomplete and the
+ number of tests requested is unknown. This is a
+ fatal issue that blocks any test execution in a class.
+
+ A class level error record is generated.
+ Unlike `setup_class` failure, no test is considered "skipped" in this
+ case as execution stage never started.
+ """
+
+ class MockBaseTest(base_test.BaseTestClass):
+
+ def pre_run(self):
+ raise Exception(MSG_EXPECTED_EXCEPTION)
+
+ def logic(self, a, b):
+ pass
+
+ def test_foo(self):
+ pass
+
+ bt_cls = MockBaseTest(self.mock_test_cls_configs)
+ bt_cls.run()
+ self.assertEqual(len(bt_cls.results.requested), 0)
+ class_record = bt_cls.results.error[0]
+ self.assertEqual(class_record.test_name, 'pre_run')
+ self.assertEqual(bt_cls.results.skipped, [])
+
+ # TODO(angli): remove after the full deprecation of `setup_generated_tests`.
+ def test_setup_generated_tests(self):
+
+ class MockBaseTest(base_test.BaseTestClass):
+
+ def setup_generated_tests(self):
+ self.generate_tests(test_logic=self.logic,
+ name_func=self.name_gen,
+ arg_sets=[(1, 2), (3, 4)])
+
+ def name_gen(self, a, b):
+ return 'test_%s_%s' % (a, b)
+
+ def logic(self, a, b):
+ pass
+
+ bt_cls = MockBaseTest(self.mock_test_cls_configs)
+ bt_cls.run()
+ self.assertEqual(len(bt_cls.results.requested), 2)
+ self.assertEqual(len(bt_cls.results.passed), 2)
+ self.assertIsNone(bt_cls.results.passed[0].uid)
+ self.assertIsNone(bt_cls.results.passed[1].uid)
+ self.assertEqual(bt_cls.results.passed[0].test_name, 'test_1_2')
+ self.assertEqual(bt_cls.results.passed[1].test_name, 'test_3_4')
+
+ # TODO(angli): remove after the full deprecation of `setup_generated_tests`.
def test_setup_generated_tests_failure(self):
"""Test code path for setup_generated_tests failure.
@@ -1987,14 +2044,14 @@ class BaseTestTest(unittest.TestCase):
bt_cls.run()
self.assertEqual(len(bt_cls.results.requested), 0)
class_record = bt_cls.results.error[0]
- self.assertEqual(class_record.test_name, 'setup_generated_tests')
+ self.assertEqual(class_record.test_name, 'pre_run')
self.assertEqual(bt_cls.results.skipped, [])
def test_generate_tests_run(self):
class MockBaseTest(base_test.BaseTestClass):
- def setup_generated_tests(self):
+ def pre_run(self):
self.generate_tests(test_logic=self.logic,
name_func=self.name_gen,
arg_sets=[(1, 2), (3, 4)])
@@ -2018,7 +2075,7 @@ class BaseTestTest(unittest.TestCase):
class MockBaseTest(base_test.BaseTestClass):
- def setup_generated_tests(self):
+ def pre_run(self):
self.generate_tests(test_logic=self.logic,
name_func=self.name_gen,
uid_func=self.uid_logic,
@@ -2042,7 +2099,7 @@ class BaseTestTest(unittest.TestCase):
class MockBaseTest(base_test.BaseTestClass):
- def setup_generated_tests(self):
+ def pre_run(self):
self.generate_tests(test_logic=self.logic,
name_func=self.name_gen,
uid_func=self.uid_logic,
@@ -2068,7 +2125,7 @@ class BaseTestTest(unittest.TestCase):
class MockBaseTest(base_test.BaseTestClass):
- def setup_generated_tests(self):
+ def pre_run(self):
self.generate_tests(test_logic=self.logic,
name_func=self.name_gen,
arg_sets=[(1, 2), (3, 4)])
@@ -2085,7 +2142,7 @@ class BaseTestTest(unittest.TestCase):
self.assertEqual(len(bt_cls.results.passed), 1)
self.assertEqual(bt_cls.results.passed[0].test_name, 'test_3_4')
- def test_generate_tests_call_outside_of_setup_generated_tests(self):
+ def test_generate_tests_call_outside_of_pre_run(self):
class MockBaseTest(base_test.BaseTestClass):
@@ -2107,7 +2164,8 @@ class BaseTestTest(unittest.TestCase):
self.assertEqual(actual_record.test_name, "test_ha")
self.assertEqual(
actual_record.details,
- '"generate_tests" cannot be called outside of setup_generated_tests')
+ "'generate_tests' cannot be called outside of the followin"
+ "g functions: ['pre_run', 'setup_generated_tests'].")
expected_summary = ("Error 1, Executed 1, Failed 0, Passed 0, "
"Requested 1, Skipped 0")
self.assertEqual(bt_cls.results.summary_str(), expected_summary)
@@ -2116,7 +2174,7 @@ class BaseTestTest(unittest.TestCase):
class MockBaseTest(base_test.BaseTestClass):
- def setup_generated_tests(self):
+ def pre_run(self):
self.generate_tests(test_logic=self.logic,
name_func=self.name_gen,
arg_sets=[(1, 2), (3, 4)])
@@ -2130,7 +2188,7 @@ class BaseTestTest(unittest.TestCase):
bt_cls = MockBaseTest(self.mock_test_cls_configs)
bt_cls.run()
actual_record = bt_cls.results.error[0]
- self.assertEqual(actual_record.test_name, "setup_generated_tests")
+ self.assertEqual(actual_record.test_name, "pre_run")
self.assertEqual(
actual_record.details,
'During test generation of "logic": Test name "ha" already exists'
@@ -2141,6 +2199,7 @@ class BaseTestTest(unittest.TestCase):
def test_write_user_data(self):
content = {'a': 1}
+ original_content = content.copy()
class MockBaseTest(base_test.BaseTestClass):
@@ -2158,7 +2217,9 @@ class BaseTestTest(unittest.TestCase):
continue
hit = True
self.assertEqual(c['a'], content['a'])
+ self.assertIn('timestamp', c)
self.assertIsNotNone(c['timestamp'])
+ self.assertEqual(content, original_content, 'Content arg was mutated.')
self.assertTrue(hit)
def test_record_controller_info(self):
@@ -2300,11 +2361,10 @@ class BaseTestTest(unittest.TestCase):
def _run_test_logic(self, arg):
pass
- def setup_generated_tests(self):
- self.generate_tests(
- self._run_test_logic,
- name_func=lambda arg: f'test_generated_{arg}',
- arg_sets=[(1,)])
+ def pre_run(self):
+ self.generate_tests(self._run_test_logic,
+ name_func=lambda arg: f'test_generated_{arg}',
+ arg_sets=[(1,)])
bt_cls = MockBaseTest(self.mock_test_cls_configs)
bt_cls.run()
@@ -2480,7 +2540,8 @@ class BaseTestTest(unittest.TestCase):
def test_retry_generated_test_last_pass(self):
max_count = 3
mock_action = mock.MagicMock(
- side_effect = [Exception('Fail 1'), Exception('Fail 2'), None])
+ side_effect=[Exception('Fail 1'),
+ Exception('Fail 2'), None])
class MockBaseTest(base_test.BaseTestClass):
@@ -2488,11 +2549,10 @@ class BaseTestTest(unittest.TestCase):
def _run_test_logic(self, arg):
mock_action()
- def setup_generated_tests(self):
- self.generate_tests(
- self._run_test_logic,
- name_func=lambda arg: f'test_generated_{arg}',
- arg_sets=[(1,)])
+ def pre_run(self):
+ self.generate_tests(self._run_test_logic,
+ name_func=lambda arg: f'test_generated_{arg}',
+ arg_sets=[(1,)])
bt_cls = MockBaseTest(self.mock_test_cls_configs)
bt_cls.run()
diff --git a/tests/mobly/controllers/android_device_lib/adb_test.py b/tests/mobly/controllers/android_device_lib/adb_test.py
index 94e3975..a013959 100755
--- a/tests/mobly/controllers/android_device_lib/adb_test.py
+++ b/tests/mobly/controllers/android_device_lib/adb_test.py
@@ -505,6 +505,17 @@ class AdbTest(unittest.TestCase):
stderr=None,
timeout=adb.DEFAULT_GETPROP_TIMEOUT_SEC)
+ def test_getprop_custom_timeout(self):
+ timeout_s = adb.DEFAULT_GETPROP_TIMEOUT_SEC * 2
+ with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
+ mock_exec_cmd.return_value = b'blah'
+ self.assertEqual(adb.AdbProxy().getprop('haha', timeout=timeout_s),
+ 'blah')
+ mock_exec_cmd.assert_called_once_with(['adb', 'shell', 'getprop', 'haha'],
+ shell=False,
+ stderr=None,
+ timeout=timeout_s)
+
def test__parse_getprop_output_special_values(self):
mock_adb_output = (
b'[selinux.restorecon_recursive]: [/data/misc_ce/10]\n'
@@ -758,8 +769,8 @@ class AdbTest(unittest.TestCase):
shell=False,
timeout=None,
stderr=None)
- self.assertEqual(mock_sleep.call_count, 2)
- mock_sleep.assert_called_with(10)
+ self.assertEqual(mock_sleep.call_count, adb.ADB_ROOT_RETRY_ATTEMPTS - 1)
+ mock_sleep.assert_has_calls([mock.call(10), mock.call(20)])
def test_has_shell_command_called_correctly(self):
with mock.patch.object(adb.AdbProxy, '_exec_cmd') as mock_exec_cmd:
diff --git a/tests/mobly/controllers/android_device_lib/callback_handler_test.py b/tests/mobly/controllers/android_device_lib/callback_handler_test.py
index 27f27b2..a30408f 100755
--- a/tests/mobly/controllers/android_device_lib/callback_handler_test.py
+++ b/tests/mobly/controllers/android_device_lib/callback_handler_test.py
@@ -47,7 +47,7 @@ class CallbackHandlerTest(unittest.TestCase):
method_name=None,
ad=mock.Mock())
self.assertEqual(handler.callback_id, MOCK_CALLBACK_ID)
- with self.assertRaisesRegex(AttributeError, "can't set attribute"):
+ with self.assertRaises(AttributeError):
handler.callback_id = 'ha'
def test_event_dict_to_snippet_event(self):
diff --git a/tests/mobly/controllers/android_device_lib/callback_handler_v2_test.py b/tests/mobly/controllers/android_device_lib/callback_handler_v2_test.py
new file mode 100644
index 0000000..b598cae
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/callback_handler_v2_test.py
@@ -0,0 +1,152 @@
+# Copyright 2022 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.
+"""Unit tests for callback_handler_v2.CallbackHandlerV2."""
+
+import unittest
+from unittest import mock
+
+from mobly.controllers.android_device_lib import callback_handler_v2
+from mobly.snippet import callback_event
+from mobly.snippet import errors
+
+MOCK_CALLBACK_ID = '2-1'
+MOCK_RAW_EVENT = {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'exampleData': "Here's a simple event.",
+ 'successful': True,
+ 'secretNumber': 12
+ }
+}
+
+
+class CallbackHandlerV2Test(unittest.TestCase):
+ """Unit tests for callback_handler_v2.CallbackHandlerV2."""
+
+ def _make_callback_handler(self,
+ callback_id=None,
+ event_client=None,
+ ret_value=None,
+ method_name=None,
+ device=None,
+ rpc_max_timeout_sec=600,
+ default_timeout_sec=120):
+ return callback_handler_v2.CallbackHandlerV2(
+ callback_id=callback_id,
+ event_client=event_client,
+ ret_value=ret_value,
+ method_name=method_name,
+ device=device,
+ rpc_max_timeout_sec=rpc_max_timeout_sec,
+ default_timeout_sec=default_timeout_sec)
+
+ def assert_event_correct(self, actual_event, expected_raw_event_dict):
+ expected_event = callback_event.from_dict(expected_raw_event_dict)
+ self.assertEqual(str(actual_event), str(expected_event))
+
+ def test_wait_and_get(self):
+ mock_event_client = mock.Mock()
+ mock_event_client.eventWaitAndGet = mock.Mock(return_value=MOCK_RAW_EVENT)
+ handler = self._make_callback_handler(callback_id=MOCK_CALLBACK_ID,
+ event_client=mock_event_client)
+ event = handler.waitAndGet('ha')
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+ mock_event_client.eventWaitAndGet.assert_called_once_with(
+ MOCK_CALLBACK_ID, 'ha', mock.ANY)
+
+ def test_wait_and_get_timeout_arg_transform(self):
+ mock_event_client = mock.Mock()
+ mock_event_client.eventWaitAndGet = mock.Mock(return_value=MOCK_RAW_EVENT)
+ handler = self._make_callback_handler(event_client=mock_event_client)
+
+ wait_and_get_timeout_sec = 10
+ expected_rpc_timeout_ms = 10000
+ _ = handler.waitAndGet('ha', timeout=wait_and_get_timeout_sec)
+ mock_event_client.eventWaitAndGet.assert_called_once_with(
+ mock.ANY, mock.ANY, expected_rpc_timeout_ms)
+
+ def test_wait_for_event(self):
+ mock_event_client = mock.Mock()
+ handler = self._make_callback_handler(callback_id=MOCK_CALLBACK_ID,
+ event_client=mock_event_client)
+
+ event_should_ignore = {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'successful': False,
+ }
+ }
+ mock_event_client.eventWaitAndGet.side_effect = [
+ event_should_ignore, MOCK_RAW_EVENT
+ ]
+
+ def some_condition(event):
+ return event.data['successful']
+
+ event = handler.waitForEvent('AsyncTaskResult', some_condition, 0.01)
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+ mock_event_client.eventWaitAndGet.assert_has_calls([
+ mock.call(MOCK_CALLBACK_ID, 'AsyncTaskResult', mock.ANY),
+ mock.call(MOCK_CALLBACK_ID, 'AsyncTaskResult', mock.ANY),
+ ])
+
+ def test_get_all(self):
+ mock_event_client = mock.Mock()
+ handler = self._make_callback_handler(callback_id=MOCK_CALLBACK_ID,
+ event_client=mock_event_client)
+
+ mock_event_client.eventGetAll = mock.Mock(
+ return_value=[MOCK_RAW_EVENT, MOCK_RAW_EVENT])
+
+ all_events = handler.getAll('ha')
+ self.assertEqual(len(all_events), 2)
+ for event in all_events:
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+
+ mock_event_client.eventGetAll.assert_called_once_with(
+ MOCK_CALLBACK_ID, 'ha')
+
+ def test_wait_and_get_timeout_message_pattern_matches(self):
+ mock_event_client = mock.Mock()
+ android_snippet_timeout_msg = (
+ 'com.google.android.mobly.snippet.event.EventSnippet$'
+ 'EventSnippetException: timeout.')
+ mock_event_client.eventWaitAndGet = mock.Mock(
+ side_effect=errors.ApiError(mock.Mock(), android_snippet_timeout_msg))
+ handler = self._make_callback_handler(event_client=mock_event_client,
+ method_name='test_method')
+
+ expected_msg = ('Timed out after waiting .*s for event "ha" triggered by '
+ 'test_method .*')
+ with self.assertRaisesRegex(errors.CallbackHandlerTimeoutError,
+ expected_msg):
+ handler.waitAndGet('ha')
+
+ def test_wait_and_get_reraise_if_pattern_not_match(self):
+ mock_event_client = mock.Mock()
+ snippet_timeout_msg = 'Snippet executed with error.'
+ mock_event_client.eventWaitAndGet = mock.Mock(
+ side_effect=errors.ApiError(mock.Mock(), snippet_timeout_msg))
+ handler = self._make_callback_handler(event_client=mock_event_client)
+
+ with self.assertRaisesRegex(errors.ApiError, snippet_timeout_msg):
+ handler.waitAndGet('ha')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/fastboot_test.py b/tests/mobly/controllers/android_device_lib/fastboot_test.py
new file mode 100644
index 0000000..6e59dea
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/fastboot_test.py
@@ -0,0 +1,43 @@
+# Copyright 2022 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 fastboot
+
+
+class FastbootTest(unittest.TestCase):
+ """Unit tests for mobly.controllers.android_device_lib.adb."""
+
+ @mock.patch('mobly.controllers.android_device_lib.fastboot.Popen')
+ @mock.patch('logging.debug')
+ def test_fastboot_commands_and_results_are_logged_to_debug_log(
+ self, mock_debug_logger, mock_popen):
+ expected_stdout = 'stdout'
+ expected_stderr = b'stderr'
+ mock_popen.return_value.communicate = mock.Mock(
+ return_value=(expected_stdout, expected_stderr))
+ mock_popen.return_value.returncode = 123
+
+ fastboot.FastbootProxy().fake_command('extra', 'flags')
+
+ mock_debug_logger.assert_called_with(
+ 'cmd: %s, stdout: %s, stderr: %s, ret: %s',
+ '\'fastboot fake-command extra flags\'', expected_stdout,
+ expected_stderr, 123)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py b/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
index 96b52cb..4cbeb35 100755
--- a/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
+++ b/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
@@ -337,6 +337,22 @@ class JsonRpcClientBaseTest(jsonrpc_client_test_base.JsonRpcClientTestBase):
testing_rpc_response[:jsonrpc_client_base._MAX_RPC_RESP_LOGGING_LENGTH],
resp_len - jsonrpc_client_base._MAX_RPC_RESP_LOGGING_LENGTH)
+ def test_close_scoket_connection(self):
+ client = FakeRpcClient()
+ mock_conn = mock.Mock()
+ client._conn = mock_conn
+
+ client.close_socket_connection()
+ mock_conn.close.assert_called_once()
+ self.assertIsNone(client._conn)
+
+ def test_close_scoket_connection_without_connection(self):
+ client = FakeRpcClient()
+ client._conn = None
+
+ client.close_socket_connection()
+ self.assertIsNone(client._conn)
+
if __name__ == '__main__':
unittest.main()
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 469ce4e..162847b 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
@@ -18,7 +18,7 @@ from unittest import mock
from mobly.controllers.android_device_lib.services import snippet_management_service
MOCK_PACKAGE = 'com.mock.package'
-SNIPPET_CLIENT_CLASS_PATH = 'mobly.controllers.android_device_lib.snippet_client.SnippetClient'
+SNIPPET_CLIENT_V2_CLASS_PATH = 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2'
class SnippetManagementServiceTest(unittest.TestCase):
@@ -33,7 +33,7 @@ class SnippetManagementServiceTest(unittest.TestCase):
manager.stop()
self.assertFalse(manager.is_alive)
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_get_snippet_client(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
@@ -41,28 +41,28 @@ class SnippetManagementServiceTest(unittest.TestCase):
manager.add_snippet_client('foo', MOCK_PACKAGE)
self.assertEqual(manager.get_snippet_client('foo'), mock_client)
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_get_snippet_client_fail(self, _):
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
self.assertIsNone(manager.get_snippet_client('foo'))
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_stop_with_live_client(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
manager.add_snippet_client('foo', MOCK_PACKAGE)
- mock_client.start_app_and_connect.assert_called_once_with()
+ mock_client.initialize.assert_called_once_with()
manager.stop()
- mock_client.stop_app.assert_called_once_with()
- mock_client.stop_app.reset_mock()
+ mock_client.stop.assert_called_once_with()
+ mock_client.stop.reset_mock()
mock_client.is_alive = False
self.assertFalse(manager.is_alive)
manager.stop()
- mock_client.stop_app.assert_not_called()
+ mock_client.stop.assert_not_called()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_add_snippet_client_dup_name(self, _):
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
@@ -72,7 +72,7 @@ class SnippetManagementServiceTest(unittest.TestCase):
with self.assertRaisesRegex(snippet_management_service.Error, msg):
manager.add_snippet_client('foo', MOCK_PACKAGE + 'ha')
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_add_snippet_client_dup_package(self, mock_class):
mock_client = mock_class.return_value
mock_client.package = MOCK_PACKAGE
@@ -84,7 +84,7 @@ class SnippetManagementServiceTest(unittest.TestCase):
with self.assertRaisesRegex(snippet_management_service.Error, msg):
manager.add_snippet_client('bar', MOCK_PACKAGE)
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_remove_snippet_client(self, mock_class):
mock_client = mock.MagicMock()
mock_class.return_value = mock_client
@@ -96,7 +96,7 @@ class SnippetManagementServiceTest(unittest.TestCase):
with self.assertRaisesRegex(snippet_management_service.Error, msg):
manager.foo.do_something()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_remove_snippet_client(self, mock_class):
mock_client = mock.MagicMock()
mock_class.return_value = mock_client
@@ -107,31 +107,31 @@ class SnippetManagementServiceTest(unittest.TestCase):
'No snippet client is registered with name "foo".'):
manager.remove_snippet_client('foo')
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_start_with_live_service(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
manager.add_snippet_client('foo', MOCK_PACKAGE)
- mock_client.start_app_and_connect.reset_mock()
+ mock_client.initialize.reset_mock()
mock_client.is_alive = True
manager.start()
- mock_client.start_app_and_connect.assert_not_called()
+ mock_client.initialize.assert_not_called()
self.assertTrue(manager.is_alive)
mock_client.is_alive = False
manager.start()
- mock_client.start_app_and_connect.assert_called_once_with()
+ mock_client.initialize.assert_called_once_with()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_pause(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
mock.MagicMock())
manager.add_snippet_client('foo', MOCK_PACKAGE)
manager.pause()
- mock_client.disconnect.assert_called_once_with()
+ mock_client.close_connection.assert_called_once_with()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_resume_positive_case(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
@@ -139,9 +139,9 @@ class SnippetManagementServiceTest(unittest.TestCase):
manager.add_snippet_client('foo', MOCK_PACKAGE)
mock_client.is_alive = False
manager.resume()
- mock_client.restore_app_connection.assert_called_once_with()
+ mock_client.restore_server_connection.assert_called_once_with()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_resume_negative_case(self, mock_class):
mock_client = mock_class.return_value
manager = snippet_management_service.SnippetManagementService(
@@ -149,9 +149,9 @@ class SnippetManagementServiceTest(unittest.TestCase):
manager.add_snippet_client('foo', MOCK_PACKAGE)
mock_client.is_alive = True
manager.resume()
- mock_client.restore_app_connection.assert_not_called()
+ mock_client.restore_server_connection.assert_not_called()
- @mock.patch(SNIPPET_CLIENT_CLASS_PATH)
+ @mock.patch(SNIPPET_CLIENT_V2_CLASS_PATH)
def test_attribute_access(self, mock_class):
mock_client = mock.MagicMock()
mock_class.return_value = mock_client
diff --git a/tests/mobly/controllers/android_device_lib/snippet_client_test.py b/tests/mobly/controllers/android_device_lib/snippet_client_test.py
index 6bb2f87..53da1ae 100755
--- a/tests/mobly/controllers/android_device_lib/snippet_client_test.py
+++ b/tests/mobly/controllers/android_device_lib/snippet_client_test.py
@@ -178,6 +178,81 @@ class SnippetClientTest(jsonrpc_client_test_base.JsonRpcClientTestBase):
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.'
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 b97774b..1943abb 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
@@ -13,6 +13,7 @@
# limitations under the License.
"""Unit tests for mobly.controllers.android_device_lib.snippet_client_v2."""
+import socket
import unittest
from unittest import mock
@@ -25,19 +26,79 @@ from tests.lib import mock_android_device
MOCK_PACKAGE_NAME = 'some.package.name'
MOCK_SERVER_PATH = f'{MOCK_PACKAGE_NAME}/{snippet_client_v2._INSTRUMENTATION_RUNNER_PACKAGE}'
MOCK_USER_ID = 0
+MOCK_DEVICE_PORT = 1234
+
+
+class _MockAdbProxy(mock_android_device.MockAdbProxy):
+ """Mock class of adb proxy which covers all the calls used by snippet clients.
+
+ To enable testing snippet clients, this class extends the functionality of
+ base class from the following aspects:
+ * Records the arguments of all the calls to the shell method and forward
+ method.
+ * Handles the adb calls to stop the snippet server in the shell function
+ properly.
+
+
+ Attributes:
+ mock_shell_func: mock.Mock, used for recording the calls to the shell
+ method.
+ mock_forward_func: mock.Mock, used for recording the calls to the forward
+ method.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """Initializes the instance of _MockAdbProxy."""
+ super().__init__(*args, **kwargs)
+ self.mock_shell_func = mock.Mock()
+ self.mock_forward_func = mock.Mock()
+
+ def shell(self, *args, **kwargs):
+ """Mock `shell` of mobly.controllers.android_device_lib.adb.AdbProxy."""
+ # Record all the call args
+ self.mock_shell_func(*args, **kwargs)
+
+ # Handle the server stop command properly
+ if f'am instrument --user 0 -w -e action stop {MOCK_SERVER_PATH}' in args:
+ return b'OK (0 tests)'
+
+ # For other commands, hand it over to the base class.
+ return super().shell(*args, **kwargs)
+
+ def forward(self, *args, **kwargs):
+ """Mock `forward` of mobly.controllers.android_device_lib.adb.AdbProxy."""
+ self.mock_forward_func(*args, **kwargs)
+
+
+def _setup_mock_socket_file(mock_socket_create_conn, resp):
+ """Sets up a mock socket file from the mock connection.
+
+ Args:
+ mock_socket_create_conn: The mock method for creating a socket connection.
+ resp: iterable, the side effect of the `readline` function of the mock
+ socket file.
+
+ Returns:
+ The mock socket file that will be injected into the code.
+ """
+ fake_file = mock.Mock()
+ fake_file.readline.side_effect = resp
+ fake_conn = mock.Mock()
+ fake_conn.makefile.return_value = fake_file
+ mock_socket_create_conn.return_value = fake_conn
+ return fake_file
class SnippetClientV2Test(unittest.TestCase):
"""Unit tests for SnippetClientV2."""
def _make_client(self, adb_proxy=None, mock_properties=None):
- adb_proxy = adb_proxy or mock_android_device.MockAdbProxy(
- instrumented_packages=[
- (MOCK_PACKAGE_NAME,
- snippet_client_v2._INSTRUMENTATION_RUNNER_PACKAGE,
- MOCK_PACKAGE_NAME)
- ],
- mock_properties=mock_properties)
+ adb_proxy = adb_proxy or _MockAdbProxy(instrumented_packages=[
+ (MOCK_PACKAGE_NAME, snippet_client_v2._INSTRUMENTATION_RUNNER_PACKAGE,
+ MOCK_PACKAGE_NAME)
+ ],
+ mock_properties=mock_properties)
+ self.adb = adb_proxy
device = mock.Mock()
device.adb = adb_proxy
@@ -48,6 +109,7 @@ class SnippetClientV2Test(unittest.TestCase):
'build_version_sdk':
adb_proxy.getprop('ro.build.version.sdk'),
}
+ self.device = device
self.client = snippet_client_v2.SnippetClientV2(MOCK_PACKAGE_NAME, device)
@@ -56,11 +118,223 @@ class SnippetClientV2Test(unittest.TestCase):
mock_properties.update(extra_properties)
self._make_client(mock_properties=mock_properties)
- def _mock_server_process_starting_response(self, mock_start_subprocess,
- resp_lines):
+ def _mock_server_process_starting_response(self,
+ mock_start_subprocess,
+ resp_lines=None):
+ resp_lines = resp_lines or [
+ b'SNIPPET START, PROTOCOL 1 0', b'SNIPPET SERVING, PORT 1234'
+ ]
mock_proc = mock_start_subprocess.return_value
mock_proc.stdout.readline.side_effect = resp_lines
+ def _make_client_and_mock_socket_conn(self,
+ mock_socket_create_conn,
+ socket_resp=None,
+ device_port=MOCK_DEVICE_PORT,
+ adb_proxy=None,
+ mock_properties=None,
+ set_counter=True):
+ """Makes the snippet client and mocks the socket connection."""
+ self._make_client(adb_proxy, mock_properties)
+
+ if socket_resp is None:
+ socket_resp = [b'{"status": true, "uid": 1}']
+ self.mock_socket_file = _setup_mock_socket_file(mock_socket_create_conn,
+ socket_resp)
+ self.client.device_port = device_port
+ self.socket_conn = mock_socket_create_conn.return_value
+ if set_counter:
+ self.client._counter = self.client._id_counter()
+
+ def _assert_client_resources_released(self, mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_get_port):
+ """Asserts the resources had been released before the client stopped."""
+ self.assertIs(self.client._proc, None)
+ self.adb.mock_shell_func.assert_any_call(
+ f'am instrument --user {MOCK_USER_ID} -w -e action stop '
+ f'{MOCK_SERVER_PATH}')
+ mock_stop_standing_subprocess.assert_called_once_with(
+ mock_start_subprocess.return_value)
+ self.assertFalse(self.client.is_alive)
+ self.assertIs(self.client._conn, None)
+ self.socket_conn.close.assert_called()
+ self.assertIs(self.client.host_port, None)
+ self.adb.mock_forward_func.assert_any_call(
+ ['--remove', f'tcp:{mock_get_port.return_value}'])
+ self.assertIsNone(self.client._event_client)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_the_whole_lifecycle_with_a_sync_rpc(self, mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_socket_create_conn,
+ mock_get_port):
+ """Tests the whole lifecycle of the client with sending a sync RPC."""
+ socket_resp = [
+ b'{"status": true, "uid": 1}',
+ b'{"id": 0, "result": 123, "error": null, "callback": null}',
+ ]
+ expected_socket_writes = [
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ mock.call(b'{"id": 0, "method": "some_sync_rpc", '
+ b'"params": [1, 2, "hello"]}\n'),
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn,
+ socket_resp,
+ set_counter=False)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.initialize()
+ rpc_result = self.client.some_sync_rpc(1, 2, 'hello')
+ self.client.stop()
+
+ self._assert_client_resources_released(mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_get_port)
+
+ self.assertListEqual(self.mock_socket_file.write.call_args_list,
+ expected_socket_writes)
+ self.assertEqual(rpc_result, 123)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.callback_handler_v2.'
+ 'CallbackHandlerV2')
+ def test_the_whole_lifecycle_with_an_async_rpc(self, mock_callback_class,
+ mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_socket_create_conn,
+ mock_get_port):
+ """Tests the whole lifecycle of the client with sending an async RPC."""
+ mock_socket_resp = [
+ b'{"status": true, "uid": 1}',
+ b'{"id": 0, "result": 123, "error": null, "callback": "1-0"}',
+ b'{"status": true, "uid": 1}',
+ ]
+ expected_socket_writes = [
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ mock.call(b'{"id": 0, "method": "some_async_rpc", '
+ b'"params": [1, 2, "async"]}\n'),
+ mock.call(b'{"cmd": "continue", "uid": 1}\n'),
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn,
+ mock_socket_resp,
+ set_counter=False)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.initialize()
+ rpc_result = self.client.some_async_rpc(1, 2, 'async')
+ event_client = self.client._event_client
+ self.client.stop()
+
+ self._assert_client_resources_released(mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_get_port)
+
+ self.assertListEqual(self.mock_socket_file.write.call_args_list,
+ expected_socket_writes)
+ mock_callback_class.assert_called_with(
+ callback_id='1-0',
+ event_client=event_client,
+ ret_value=123,
+ method_name='some_async_rpc',
+ device=self.device,
+ rpc_max_timeout_sec=snippet_client_v2._SOCKET_READ_TIMEOUT,
+ default_timeout_sec=snippet_client_v2._CALLBACK_DEFAULT_TIMEOUT_SEC)
+ self.assertIs(rpc_result, mock_callback_class.return_value)
+ self.assertIsNone(event_client.host_port, None)
+ self.assertIsNone(event_client.device_port, None)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.callback_handler_v2.'
+ 'CallbackHandlerV2')
+ def test_the_whole_lifecycle_with_multiple_rpcs(self, mock_callback_class,
+ mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_socket_create_conn,
+ mock_get_port):
+ """Tests the whole lifecycle of the client with sending multiple RPCs."""
+ # Prepare the test
+ mock_socket_resp = [
+ b'{"status": true, "uid": 1}',
+ b'{"id": 0, "result": 123, "error": null, "callback": null}',
+ b'{"id": 1, "result": 456, "error": null, "callback": "1-0"}',
+ # Response for starting the event client
+ b'{"status": true, "uid": 1}',
+ b'{"id": 2, "result": 789, "error": null, "callback": null}',
+ b'{"id": 3, "result": 321, "error": null, "callback": "2-0"}',
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn,
+ mock_socket_resp,
+ set_counter=False)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ rpc_results_expected = [
+ 123,
+ mock.Mock(),
+ 789,
+ mock.Mock(),
+ ]
+ # Extract the two mock objects to use as return values of callback handler
+ # class
+ mock_callback_class.side_effect = [
+ rpc_results_expected[1], rpc_results_expected[3]
+ ]
+
+ # Run tests
+ rpc_results = []
+ self.client.initialize()
+ rpc_results.append(self.client.some_sync_rpc(1, 2, 'hello'))
+ rpc_results.append(self.client.some_async_rpc(3, 4, 'async'))
+ rpc_results.append(self.client.some_sync_rpc(5, 'hello'))
+ rpc_results.append(self.client.some_async_rpc(6, 'async'))
+ event_client = self.client._event_client
+ self.client.stop()
+
+ # Assertions
+ mock_callback_class_calls_expected = [
+ mock.call(callback_id='1-0',
+ event_client=event_client,
+ ret_value=456,
+ method_name='some_async_rpc',
+ device=self.device,
+ rpc_max_timeout_sec=snippet_client_v2._SOCKET_READ_TIMEOUT,
+ default_timeout_sec=(
+ snippet_client_v2._CALLBACK_DEFAULT_TIMEOUT_SEC)),
+ mock.call(
+ callback_id='2-0',
+ event_client=event_client,
+ ret_value=321,
+ method_name='some_async_rpc',
+ device=self.device,
+ rpc_max_timeout_sec=snippet_client_v2._SOCKET_READ_TIMEOUT,
+ default_timeout_sec=snippet_client_v2._CALLBACK_DEFAULT_TIMEOUT_SEC)
+ ]
+ self.assertListEqual(rpc_results, rpc_results_expected)
+ mock_callback_class.assert_has_calls(mock_callback_class_calls_expected)
+ self._assert_client_resources_released(mock_start_subprocess,
+ mock_stop_standing_subprocess,
+ mock_get_port)
+ self.assertIsNone(event_client.host_port, None)
+ self.assertIsNone(event_client.device_port, None)
+
def test_check_app_installed_normally(self):
"""Tests that app checker runs normally when app installed correctly."""
self._make_client()
@@ -68,16 +342,14 @@ class SnippetClientV2Test(unittest.TestCase):
def test_check_app_installed_fail_app_not_installed(self):
"""Tests that app checker fails without installing app."""
- self._make_client(mock_android_device.MockAdbProxy())
+ self._make_client(_MockAdbProxy())
expected_msg = f'.* {MOCK_PACKAGE_NAME} is not installed.'
with self.assertRaisesRegex(errors.ServerStartPreCheckError, expected_msg):
self.client._validate_snippet_app_on_device()
def test_check_app_installed_fail_not_instrumented(self):
"""Tests that app checker fails without instrumenting app."""
- self._make_client(
- mock_android_device.MockAdbProxy(
- installed_packages=[MOCK_PACKAGE_NAME]))
+ self._make_client(_MockAdbProxy(installed_packages=[MOCK_PACKAGE_NAME]))
expected_msg = (
f'.* {MOCK_PACKAGE_NAME} is installed, but it is not instrumented.')
with self.assertRaisesRegex(errors.ServerStartPreCheckError, expected_msg):
@@ -86,7 +358,7 @@ class SnippetClientV2Test(unittest.TestCase):
def test_check_app_installed_fail_instrumentation_not_installed(self):
"""Tests that app checker fails without installing instrumentation."""
self._make_client(
- mock_android_device.MockAdbProxy(instrumented_packages=[(
+ _MockAdbProxy(instrumented_packages=[(
MOCK_PACKAGE_NAME,
snippet_client_v2._INSTRUMENTATION_RUNNER_PACKAGE,
'not.installed')]))
@@ -94,53 +366,44 @@ class SnippetClientV2Test(unittest.TestCase):
with self.assertRaisesRegex(errors.ServerStartPreCheckError, expected_msg):
self.client._validate_snippet_app_on_device()
- @mock.patch.object(mock_android_device.MockAdbProxy, 'shell')
- def test_disable_hidden_api_normally(self, mock_shell_func):
+ def test_disable_hidden_api_normally(self):
"""Tests the disabling hidden api process works normally."""
self._make_client_with_extra_adb_properties({
'ro.build.version.codename': 'S',
'ro.build.version.sdk': '31',
})
- self.client._device.is_rootable = True
+ self.device.is_rootable = True
self.client._disable_hidden_api_blocklist()
- mock_shell_func.assert_called_with(
+ self.adb.mock_shell_func.assert_called_with(
'settings put global hidden_api_blacklist_exemptions "*"')
- @mock.patch.object(mock_android_device.MockAdbProxy, 'shell')
- def test_disable_hidden_api_low_sdk(self, mock_shell_func):
+ def test_disable_hidden_api_low_sdk(self):
"""Tests it doesn't disable hidden api with low SDK."""
self._make_client_with_extra_adb_properties({
'ro.build.version.codename': 'O',
'ro.build.version.sdk': '26',
})
- self.client._device.is_rootable = True
+ self.device.is_rootable = True
self.client._disable_hidden_api_blocklist()
- mock_shell_func.assert_not_called()
+ self.adb.mock_shell_func.assert_not_called()
- @mock.patch.object(mock_android_device.MockAdbProxy, 'shell')
- def test_disable_hidden_api_non_rootable(self, mock_shell_func):
+ def test_disable_hidden_api_non_rootable(self):
"""Tests it doesn't disable hidden api with non-rootable device."""
self._make_client_with_extra_adb_properties({
'ro.build.version.codename': 'S',
'ro.build.version.sdk': '31',
})
- self.client._device.is_rootable = False
+ self.device.is_rootable = False
self.client._disable_hidden_api_blocklist()
- mock_shell_func.assert_not_called()
+ self.adb.mock_shell_func.assert_not_called()
@mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
'utils.start_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
- 'shell',
- return_value=b'setsid')
+ @mock.patch.object(_MockAdbProxy, 'shell', return_value=b'setsid')
def test_start_server_with_user_id(self, mock_adb, mock_start_subprocess):
"""Tests that `--user` is added to starting command with SDK >= 24."""
self._make_client_with_extra_adb_properties({'ro.build.version.sdk': '30'})
- self._mock_server_process_starting_response(
- mock_start_subprocess,
- resp_lines=[
- b'SNIPPET START, PROTOCOL 1 234', b'SNIPPET SERVING, PORT 1234'
- ])
+ self._mock_server_process_starting_response(mock_start_subprocess)
self.client.start_server()
start_cmd_list = [
@@ -155,17 +418,11 @@ class SnippetClientV2Test(unittest.TestCase):
@mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
'utils.start_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
- 'shell',
- return_value=b'setsid')
+ @mock.patch.object(_MockAdbProxy, 'shell', return_value=b'setsid')
def test_start_server_without_user_id(self, mock_adb, mock_start_subprocess):
"""Tests that `--user` is not added to starting command on SDK < 24."""
self._make_client_with_extra_adb_properties({'ro.build.version.sdk': '21'})
- self._mock_server_process_starting_response(
- mock_start_subprocess,
- resp_lines=[
- b'SNIPPET START, PROTOCOL 1 234', b'SNIPPET SERVING, PORT 1234'
- ])
+ self._mock_server_process_starting_response(mock_start_subprocess)
self.client.start_server()
start_cmd_list = [
@@ -179,7 +436,7 @@ class SnippetClientV2Test(unittest.TestCase):
@mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
'utils.start_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
+ @mock.patch.object(_MockAdbProxy,
'shell',
side_effect=adb.AdbError('cmd', 'stdout', 'stderr',
'ret_code'))
@@ -187,11 +444,7 @@ class SnippetClientV2Test(unittest.TestCase):
mock_start_subprocess):
"""Checks the starting server command without persisting commands."""
self._make_client()
- self._mock_server_process_starting_response(
- mock_start_subprocess,
- resp_lines=[
- b'SNIPPET START, PROTOCOL 1 234', b'SNIPPET SERVING, PORT 1234'
- ])
+ self._mock_server_process_starting_response(mock_start_subprocess)
self.client.start_server()
start_cmd_list = [
@@ -211,11 +464,7 @@ class SnippetClientV2Test(unittest.TestCase):
def test_start_server_with_nohup(self, mock_start_subprocess):
"""Checks the starting server command with nohup."""
self._make_client()
- self._mock_server_process_starting_response(
- mock_start_subprocess,
- resp_lines=[
- b'SNIPPET START, PROTOCOL 1 234', b'SNIPPET SERVING, PORT 1234'
- ])
+ self._mock_server_process_starting_response(mock_start_subprocess)
def _mocked_shell(arg):
if 'nohup' in arg:
@@ -239,11 +488,7 @@ class SnippetClientV2Test(unittest.TestCase):
def test_start_server_with_setsid(self, mock_start_subprocess):
"""Checks the starting server command with setsid."""
self._make_client()
- self._mock_server_process_starting_response(
- mock_start_subprocess,
- resp_lines=[
- b'SNIPPET START, PROTOCOL 1 234', b'SNIPPET SERVING, PORT 1234'
- ])
+ self._mock_server_process_starting_response(mock_start_subprocess)
def _mocked_shell(arg):
if 'setsid' in arg:
@@ -337,57 +582,804 @@ class SnippetClientV2Test(unittest.TestCase):
self.client.start_server()
@mock.patch('mobly.utils.stop_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
- 'shell',
- return_value=b'OK (0 tests)')
- def test_stop_server_normally(self, mock_android_device_shell,
- mock_stop_standing_subprocess):
+ def test_stop_normally(self, mock_stop_standing_subprocess):
"""Tests that stopping server process works normally."""
self._make_client()
mock_proc = mock.Mock()
self.client._proc = mock_proc
+ mock_conn = mock.Mock()
+ self.client._conn = mock_conn
+ self.client.host_port = 12345
+
self.client.stop()
+
self.assertIs(self.client._proc, None)
- mock_android_device_shell.assert_called_once_with(
+ self.adb.mock_shell_func.assert_called_once_with(
f'am instrument --user {MOCK_USER_ID} -w -e action stop '
f'{MOCK_SERVER_PATH}')
mock_stop_standing_subprocess.assert_called_once_with(mock_proc)
+ self.assertFalse(self.client.is_alive)
+ self.assertIs(self.client._conn, None)
+ mock_conn.close.assert_called_once_with()
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+ self.assertIsNone(self.client._event_client)
@mock.patch('mobly.utils.stop_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
- 'shell',
- return_value=b'OK (0 tests)')
- def test_stop_server_server_already_cleaned(self, mock_android_device_shell,
- mock_stop_standing_subprocess):
- """Tests stopping server process when subprocess is already cleaned."""
+ def test_stop_when_server_is_already_cleaned(self,
+ mock_stop_standing_subprocess):
+ """Tests that stop server process when subprocess is already cleaned."""
self._make_client()
self.client._proc = None
+ mock_conn = mock.Mock()
+ self.client._conn = mock_conn
+ self.client.host_port = 12345
+
self.client.stop()
+
self.assertIs(self.client._proc, None)
mock_stop_standing_subprocess.assert_not_called()
- mock_android_device_shell.assert_called_once_with(
+ self.adb.assert_called_once_with(
f'am instrument --user {MOCK_USER_ID} -w -e action stop '
f'{MOCK_SERVER_PATH}')
+ self.assertFalse(self.client.is_alive)
+ self.assertIs(self.client._conn, None)
+ mock_conn.close.assert_called_once_with()
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
@mock.patch('mobly.utils.stop_standing_subprocess')
- @mock.patch.object(mock_android_device.MockAdbProxy,
- 'shell',
- return_value=b'Closed with error.')
- def test_stop_server_stop_with_error(self, mock_android_device_shell,
+ def test_stop_when_conn_is_already_cleaned(self,
+ mock_stop_standing_subprocess):
+ """Tests that stop server process when the connection is already closed."""
+ self._make_client()
+ mock_proc = mock.Mock()
+ self.client._proc = mock_proc
+ self.client._conn = None
+ self.client.host_port = 12345
+
+ self.client.stop()
+
+ self.assertIs(self.client._proc, None)
+ mock_stop_standing_subprocess.assert_called_once_with(mock_proc)
+ self.adb.assert_called_once_with(
+ f'am instrument --user {MOCK_USER_ID} -w -e action stop '
+ f'{MOCK_SERVER_PATH}')
+ self.assertFalse(self.client.is_alive)
+ self.assertIs(self.client._conn, None)
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch.object(_MockAdbProxy, 'shell', return_value=b'Closed with error.')
+ def test_stop_with_device_side_error(self, mock_adb_shell,
mock_stop_standing_subprocess):
- """Tests all resources are cleaned even if stopping server has error."""
+ """Tests all resources will be cleaned when server stop throws an error."""
self._make_client()
mock_proc = mock.Mock()
self.client._proc = mock_proc
+ mock_conn = mock.Mock()
+ self.client._conn = mock_conn
+ self.client.host_port = 12345
with self.assertRaisesRegex(android_device_lib_errors.DeviceError,
'Closed with error'):
self.client.stop()
self.assertIs(self.client._proc, None)
mock_stop_standing_subprocess.assert_called_once_with(mock_proc)
- mock_android_device_shell.assert_called_once_with(
+ mock_adb_shell.assert_called_once_with(
f'am instrument --user {MOCK_USER_ID} -w -e action stop '
f'{MOCK_SERVER_PATH}')
+ self.assertFalse(self.client.is_alive)
+ self.assertIs(self.client._conn, None)
+ mock_conn.close.assert_called_once_with()
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ def test_stop_with_conn_close_error(self, mock_stop_standing_subprocess):
+ """Tests port resource will be cleaned when socket close throws an error."""
+ del mock_stop_standing_subprocess
+ self._make_client()
+ mock_proc = mock.Mock()
+ self.client._proc = mock_proc
+ mock_conn = mock.Mock()
+ # The deconstructor will call this mock function again after tests, so
+ # only throw this error when it is called the first time.
+ mock_conn.close.side_effect = (OSError('Closed with error'), None)
+ self.client._conn = mock_conn
+ self.client.host_port = 12345
+ with self.assertRaisesRegex(OSError, 'Closed with error'):
+ self.client.stop()
+
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'create_socket_connection')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'send_handshake_request')
+ def test_stop_with_event_client(self, mock_send_handshake_func,
+ mock_create_socket_conn_func,
+ mock_stop_standing_subprocess):
+ """Tests that stopping with an event client works normally."""
+ del mock_send_handshake_func
+ del mock_create_socket_conn_func
+ del mock_stop_standing_subprocess
+ self._make_client()
+ self.client.host_port = 12345
+ self.client.device_port = 45678
+ snippet_client_conn = mock.Mock()
+ self.client._conn = snippet_client_conn
+ self.client._create_event_client()
+ event_client = self.client._event_client
+ event_client_conn = mock.Mock()
+ event_client._conn = event_client_conn
+
+ self.client.stop()
+
+ # The snippet client called close method once
+ snippet_client_conn.close.assert_called_once_with()
+ # The event client called close method once
+ event_client_conn.close.assert_called_once_with()
+ self.assertIsNone(event_client._conn)
+ self.assertIsNone(event_client.host_port)
+ self.assertIsNone(event_client.device_port)
+ self.assertIsNone(self.client._event_client)
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'create_socket_connection')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'send_handshake_request')
+ def test_stop_with_event_client_stops_port_forwarding_once(
+ self, mock_send_handshake_func, mock_create_socket_conn_func,
+ mock_stop_standing_subprocess):
+ """Tests that client with an event client stops port forwarding once."""
+ del mock_send_handshake_func
+ del mock_create_socket_conn_func
+ del mock_stop_standing_subprocess
+ self._make_client()
+ self.client.host_port = 12345
+ self.client.device_port = 45678
+ self.client._create_event_client()
+ event_client = self.client._event_client
+
+ self.client.stop()
+ event_client.__del__()
+ self.client.__del__()
+
+ self.assertIsNone(self.client._event_client)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:12345'])
+
+ def test_close_connection_normally(self):
+ """Tests that closing connection works normally."""
+ self._make_client()
+ mock_conn = mock.Mock()
+ self.client._conn = mock_conn
+ self.client.host_port = 123
+
+ self.client.close_connection()
+
+ self.assertIs(self.client._conn, None)
+ self.assertIs(self.client.host_port, None)
+ mock_conn.close.assert_called_once_with()
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:123'])
+
+ def test_close_connection_when_host_port_has_been_released(self):
+ """Tests that close connection when the host port has been released."""
+ self._make_client()
+ mock_conn = mock.Mock()
+ self.client._conn = mock_conn
+ self.client.host_port = None
+
+ self.client.close_connection()
+
+ self.assertIs(self.client._conn, None)
+ self.assertIs(self.client.host_port, None)
+ mock_conn.close.assert_called_once_with()
+ self.device.adb.mock_forward_func.assert_not_called()
+
+ def test_close_connection_when_conn_have_been_closed(self):
+ """Tests that close connection when the connection has been closed."""
+ self._make_client()
+ self.client._conn = None
+ self.client.host_port = 123
+
+ self.client.close_connection()
+
+ self.assertIs(self.client._conn, None)
+ self.assertIs(self.client.host_port, None)
+ self.device.adb.mock_forward_func.assert_called_once_with(
+ ['--remove', 'tcp:123'])
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_send_sync_rpc_normally(self, mock_start_subprocess,
+ mock_socket_create_conn, mock_get_port):
+ """Tests that sending a sync RPC works normally."""
+ del mock_get_port
+ socket_resp = [
+ b'{"status": true, "uid": 1}',
+ b'{"id": 0, "result": 123, "error": null, "callback": null}',
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.make_connection()
+ rpc_result = self.client.some_rpc(1, 2, 'hello')
+
+ self.assertEqual(rpc_result, 123)
+ self.mock_socket_file.write.assert_called_with(
+ b'{"id": 0, "method": "some_rpc", "params": [1, 2, "hello"]}\n')
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.callback_handler_v2.'
+ 'CallbackHandlerV2')
+ def test_async_rpc_start_event_client(self, mock_callback_class,
+ mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests that sending an async RPC starts the event client."""
+ socket_resp = [
+ b'{"status": true, "uid": 1}',
+ b'{"id": 0, "result": 123, "error": null, "callback": "1-0"}',
+ b'{"status": true, "uid": 1}',
+ b'{"id":1,"result":"async-rpc-event","callback":null,"error":null}',
+ ]
+ socket_write_expected = [
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ mock.call(b'{"id": 0, "method": "some_async_rpc", '
+ b'"params": [1, 2, "hello"]}\n'),
+ mock.call(b'{"cmd": "continue", "uid": 1}\n'),
+ mock.call(b'{"id": 1, "method": "eventGetAll", '
+ b'"params": ["1-0", "eventName"]}\n'),
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn,
+ socket_resp,
+ set_counter=True)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.host_port = 12345
+ self.client.make_connection()
+ rpc_result = self.client.some_async_rpc(1, 2, 'hello')
+
+ mock_callback_class.assert_called_with(
+ callback_id='1-0',
+ event_client=self.client._event_client,
+ ret_value=123,
+ method_name='some_async_rpc',
+ device=self.device,
+ rpc_max_timeout_sec=snippet_client_v2._SOCKET_READ_TIMEOUT,
+ default_timeout_sec=snippet_client_v2._CALLBACK_DEFAULT_TIMEOUT_SEC)
+ self.assertIs(rpc_result, mock_callback_class.return_value)
+
+ # Ensure the event client is alive
+ self.assertTrue(self.client._event_client.is_alive)
+
+ # Ensure the event client shared the same ports and uid with main client
+ self.assertEqual(self.client._event_client.host_port, 12345)
+ self.assertEqual(self.client._event_client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(self.client._event_client.uid, self.client.uid)
+
+ # Ensure the event client has reset its own RPC id counter
+ self.assertEqual(next(self.client._counter), 1)
+ self.assertEqual(next(self.client._event_client._counter), 0)
+
+ # Ensure that event client can send RPCs
+ event_string = self.client._event_client.eventGetAll('1-0', 'eventName')
+ self.assertEqual(event_string, 'async-rpc-event')
+ self.assertListEqual(
+ self.mock_socket_file.write.call_args_list,
+ socket_write_expected,
+ )
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port')
+ def test_initialize_client_normally(self, mock_get_port,
+ mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests that initializing the client works normally."""
+ mock_get_port.return_value = 12345
+ socket_resp = [b'{"status": true, "uid": 1}']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn,
+ socket_resp,
+ set_counter=True)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.initialize()
+ self.assertTrue(self.client.is_alive)
+ self.assertEqual(self.client.uid, 1)
+ self.assertEqual(self.client.host_port, 12345)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(next(self.client._counter), 0)
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port')
+ def test_restore_event_client(self, mock_get_port, mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests restoring the event client."""
+ mock_get_port.return_value = 12345
+ socket_resp = [
+ # response of handshake when initializing the client
+ b'{"status": true, "uid": 1}',
+ # response of an async RPC
+ b'{"id": 0, "result": 123, "error": null, "callback": "1-0"}',
+ # response of starting event client
+ b'{"status": true, "uid": 1}',
+ # response of restoring server connection
+ b'{"status": true, "uid": 2}',
+ # response of restoring event client
+ b'{"status": true, "uid": 3}',
+ # response of restoring server connection
+ b'{"status": true, "uid": 4}',
+ # response of restoring event client
+ b'{"status": true, "uid": 5}',
+ ]
+ socket_write_expected = [
+ # request of handshake when initializing the client
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ # request of an async RPC
+ mock.call(b'{"id": 0, "method": "some_async_rpc", "params": []}\n'),
+ # request of starting event client
+ mock.call(b'{"cmd": "continue", "uid": 1}\n'),
+ # request of restoring server connection
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ # request of restoring event client
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ # request of restoring server connection
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ # request of restoring event client
+ mock.call(b'{"cmd": "initiate", "uid": -1}\n'),
+ ]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.make_connection()
+ callback = self.client.some_async_rpc()
+
+ # before reconnect, clients use previously selected ports
+ self.assertEqual(self.client.host_port, 12345)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(callback._event_client.host_port, 12345)
+ self.assertEqual(callback._event_client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(next(self.client._event_client._counter), 0)
+
+ # after reconnect, if host port specified, clients use specified port
+ self.client.restore_server_connection(port=54321)
+ self.assertEqual(self.client.host_port, 54321)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(callback._event_client.host_port, 54321)
+ self.assertEqual(callback._event_client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(next(self.client._event_client._counter), 0)
+
+ # after reconnect, if host port not specified, clients use selected
+ # available port
+ mock_get_port.return_value = 56789
+ self.client.restore_server_connection()
+ self.assertEqual(self.client.host_port, 56789)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(callback._event_client.host_port, 56789)
+ self.assertEqual(callback._event_client.device_port, MOCK_DEVICE_PORT)
+ self.assertEqual(next(self.client._event_client._counter), 0)
+
+ # if unable to reconnect for any reason, a
+ # errors.ServerRestoreConnectionError is raised.
+ mock_socket_create_conn.side_effect = IOError('socket timed out')
+ with self.assertRaisesRegex(
+ errors.ServerRestoreConnectionError,
+ (f'Failed to restore server connection for {MOCK_PACKAGE_NAME} at '
+ f'host port 56789, device port {MOCK_DEVICE_PORT}')):
+ self.client.restore_server_connection()
+
+ self.assertListEqual(self.mock_socket_file.write.call_args_list,
+ socket_write_expected)
+
+ @mock.patch.object(snippet_client_v2.SnippetClientV2, '_make_connection')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'send_handshake_request')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'create_socket_connection')
+ def test_restore_server_connection_with_event_client(
+ self, mock_create_socket_conn_func, mock_send_handshake_func,
+ mock_make_connection):
+ """Tests restoring server connection when the event client is not None."""
+ self._make_client()
+ event_client = snippet_client_v2.SnippetClientV2('mock-package',
+ mock.Mock())
+ self.client._event_client = event_client
+ self.client.device_port = 54321
+ self.client.uid = 5
+
+ self.client.restore_server_connection(port=12345)
+
+ mock_make_connection.assert_called_once_with()
+ self.assertEqual(event_client.host_port, 12345)
+ self.assertEqual(event_client.device_port, 54321)
+ self.assertEqual(next(event_client._counter), 0)
+ mock_create_socket_conn_func.assert_called_once_with()
+ mock_send_handshake_func.assert_called_once_with(
+ -1, snippet_client_v2.ConnectionHandshakeCommand.INIT)
+
+ @mock.patch('builtins.print')
+ def test_help_rpc_when_printing_by_default(self, mock_print):
+ """Tests the `help` method when it prints the output by default."""
+ self._make_client()
+ mock_rpc = mock.MagicMock()
+ self.client._rpc = mock_rpc
+
+ result = self.client.help()
+ mock_rpc.assert_called_once_with('help')
+ self.assertIsNone(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):
+ """Tests the `help` method when it was set not to print the output."""
+ self._make_client()
+ mock_rpc = mock.MagicMock()
+ self.client._rpc = mock_rpc
+
+ result = self.client.help(print_output=False)
+ mock_rpc.assert_called_once_with('help')
+ self.assertEqual(mock_rpc.return_value, result)
+ mock_print.assert_not_called()
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ def test_make_connection_normally(self, mock_get_port, mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests that making a connection works normally."""
+ del mock_get_port
+ socket_resp = [b'{"status": true, "uid": 1}']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.make_connection()
+ self.assertEqual(self.client.uid, 1)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.adb.mock_forward_func.assert_called_once_with(
+ ['tcp:12345', f'tcp:{MOCK_DEVICE_PORT}'])
+ mock_socket_create_conn.assert_called_once_with(
+ ('localhost', 12345), snippet_client_v2._SOCKET_CONNECTION_TIMEOUT)
+ self.socket_conn.settimeout.assert_called_once_with(
+ snippet_client_v2._SOCKET_READ_TIMEOUT)
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ def test_make_connection_with_preset_host_port(self, mock_get_port,
+ mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests that make a connection with the preset host port."""
+ del mock_get_port
+ socket_resp = [b'{"status": true, "uid": 1}']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.host_port = 23456
+ self.client.make_connection()
+ self.assertEqual(self.client.uid, 1)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ # Test that the host port for forwarding is 23456 instead of 12345
+ self.adb.mock_forward_func.assert_called_once_with(
+ ['tcp:23456', f'tcp:{MOCK_DEVICE_PORT}'])
+ mock_socket_create_conn.assert_called_once_with(
+ ('localhost', 23456), snippet_client_v2._SOCKET_CONNECTION_TIMEOUT)
+ self.socket_conn.settimeout.assert_called_once_with(
+ snippet_client_v2._SOCKET_READ_TIMEOUT)
+
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ def test_make_connection_with_ip(self, mock_get_port, mock_start_subprocess,
+ mock_socket_create_conn):
+ """Tests that make a connection with 127.0.0.1 instead of localhost."""
+ del mock_get_port
+ socket_resp = [b'{"status": true, "uid": 1}']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ mock_conn = mock_socket_create_conn.return_value
+
+ # Refuse creating socket connection with 'localhost', only accept
+ # '127.0.0.1' as address
+ def _mock_create_conn_side_effect(address, *args, **kwargs):
+ del args, kwargs
+ if address[0] == '127.0.0.1':
+ return mock_conn
+ raise ConnectionRefusedError(f'Refusing connection to {address[0]}.')
+
+ mock_socket_create_conn.side_effect = _mock_create_conn_side_effect
+
+ self.client.make_connection()
+ self.assertEqual(self.client.uid, 1)
+ self.assertEqual(self.client.device_port, MOCK_DEVICE_PORT)
+ self.adb.mock_forward_func.assert_called_once_with(
+ ['tcp:12345', f'tcp:{MOCK_DEVICE_PORT}'])
+ mock_socket_create_conn.assert_any_call(
+ ('127.0.0.1', 12345), snippet_client_v2._SOCKET_CONNECTION_TIMEOUT)
+ self.socket_conn.settimeout.assert_called_once_with(
+ snippet_client_v2._SOCKET_READ_TIMEOUT)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ def test_make_connection_io_error(self, mock_socket_create_conn,
+ mock_get_port):
+ """Tests IOError occurred trying to create a socket connection."""
+ del mock_get_port
+ mock_socket_create_conn.side_effect = IOError()
+ with self.assertRaises(IOError):
+ self._make_client()
+ self.client.device_port = 123
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ def test_make_connection_timeout(self, mock_socket_create_conn,
+ mock_get_port):
+ """Tests timeout occurred trying to create a socket connection."""
+ del mock_get_port
+ mock_socket_create_conn.side_effect = socket.timeout
+ with self.assertRaises(socket.timeout):
+ self._make_client()
+ self.client.device_port = 123
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_receives_none_handshake_response(
+ self, mock_start_subprocess, mock_socket_create_conn, mock_get_port):
+ """Tests make_connection receives None as the handshake response."""
+ del mock_get_port
+ socket_resp = [None]
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ with self.assertRaisesRegex(
+ errors.ProtocolError, errors.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_receives_empty_handshake_response(
+ self, mock_start_subprocess, mock_socket_create_conn, mock_get_port):
+ """Tests make_connection receives an empty handshake response."""
+ del mock_get_port
+ socket_resp = [b'']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ with self.assertRaisesRegex(
+ errors.ProtocolError, errors.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_receives_invalid_handshake_response(
+ self, mock_start_subprocess, mock_socket_create_conn, mock_get_port):
+ """Tests make_connection receives an invalid handshake response."""
+ del mock_get_port
+ socket_resp = [b'{"status": false, "uid": 1}']
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn, socket_resp)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+
+ self.client.make_connection()
+ self.assertEqual(self.client.uid, -1)
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_send_handshake_request_error(self,
+ mock_start_subprocess,
+ mock_socket_create_conn,
+ mock_get_port):
+ """Tests that an error occurred trying to send a handshake request."""
+ del mock_get_port
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+ self.mock_socket_file.write.side_effect = socket.error('Socket write error')
+
+ with self.assertRaisesRegex(errors.Error, 'Socket write error'):
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_receive_handshake_response_error(
+ self, mock_start_subprocess, mock_socket_create_conn, mock_get_port):
+ """Tests that an error occurred trying to receive a handshake response."""
+ del mock_get_port
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+ self.mock_socket_file.readline.side_effect = socket.error(
+ 'Socket read error')
+
+ with self.assertRaisesRegex(errors.Error, 'Socket read error'):
+ self.client.make_connection()
+
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.get_available_host_port',
+ return_value=12345)
+ @mock.patch('socket.create_connection')
+ @mock.patch('mobly.controllers.android_device_lib.snippet_client_v2.'
+ 'utils.start_standing_subprocess')
+ def test_make_connection_decode_handshake_response_bytes_error(
+ self, mock_start_subprocess, mock_socket_create_conn, mock_get_port):
+ """Tests that an error occurred trying to decode a handshake response."""
+ del mock_get_port
+ self._make_client_and_mock_socket_conn(mock_socket_create_conn)
+ self._mock_server_process_starting_response(mock_start_subprocess)
+ self.client.log = mock.Mock()
+ socket_response = bytes('{"status": false, "uid": 1}', encoding='cp037')
+ self.mock_socket_file.readline.side_effect = [socket_response]
+
+ with self.assertRaises(UnicodeError):
+ self.client.make_connection()
+
+ self.client.log.error.assert_has_calls([
+ mock.call(
+ 'Failed to decode socket response bytes using encoding utf8: %s',
+ socket_response)
+ ])
+
+ def test_rpc_sending_and_receiving(self):
+ """Test RPC sending and receiving.
+
+ Tests that when sending and receiving an RPC the correct data is used.
+ """
+ self._make_client()
+ rpc_request = '{"id": 0, "method": "some_rpc", "params": []}'
+ rpc_response_expected = ('{"id": 0, "result": 123, "error": null, '
+ '"callback": null}')
+
+ socket_write_expected = [
+ mock.call(b'{"id": 0, "method": "some_rpc", "params": []}\n')
+ ]
+ socket_response = (b'{"id": 0, "result": 123, "error": null, '
+ b'"callback": null}')
+
+ mock_socket_file = mock.Mock()
+ mock_socket_file.readline.return_value = socket_response
+ self.client._client = mock_socket_file
+
+ rpc_response = self.client.send_rpc_request(rpc_request)
+
+ self.assertEqual(rpc_response, rpc_response_expected)
+ self.assertEqual(mock_socket_file.write.call_args_list,
+ socket_write_expected)
+
+ def test_rpc_send_socket_write_error(self):
+ """Tests that an error occurred trying to write the socket file."""
+ self._make_client()
+ self.client._client = mock.Mock()
+ self.client._client.write.side_effect = socket.error('Socket write error')
+
+ rpc_request = '{"id": 0, "method": "some_rpc", "params": []}'
+ with self.assertRaisesRegex(errors.Error, 'Socket write error'):
+ self.client.send_rpc_request(rpc_request)
+
+ def test_rpc_send_socket_read_error(self):
+ """Tests that an error occurred trying to read the socket file."""
+ self._make_client()
+ self.client._client = mock.Mock()
+ self.client._client.readline.side_effect = socket.error('Socket read error')
+
+ rpc_request = '{"id": 0, "method": "some_rpc", "params": []}'
+ with self.assertRaisesRegex(errors.Error, 'Socket read error'):
+ self.client.send_rpc_request(rpc_request)
+
+ def test_rpc_send_decode_socket_response_bytes_error(self):
+ """Tests that an error occurred trying to decode the socket response."""
+ self._make_client()
+ self.client.log = mock.Mock()
+ self.client._client = mock.Mock()
+ socket_response = bytes(
+ '{"id": 0, "result": 123, "error": null, "callback": null}',
+ encoding='cp037')
+ self.client._client.readline.return_value = socket_response
+
+ rpc_request = '{"id": 0, "method": "some_rpc", "params": []}'
+ with self.assertRaises(UnicodeError):
+ self.client.send_rpc_request(rpc_request)
+
+ self.client.log.error.assert_has_calls([
+ mock.call(
+ 'Failed to decode socket response bytes using encoding utf8: %s',
+ socket_response)
+ ])
+
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'send_handshake_request')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'create_socket_connection')
+ def test_make_conn_with_forwarded_port_init(self,
+ mock_create_socket_conn_func,
+ mock_send_handshake_func):
+ """Tests make_connection_with_forwarded_port initiates a new session."""
+ self._make_client()
+ self.client._counter = None
+ self.client.make_connection_with_forwarded_port(12345, 54321)
+
+ self.assertEqual(self.client.host_port, 12345)
+ self.assertEqual(self.client.device_port, 54321)
+ self.assertEqual(next(self.client._counter), 0)
+ mock_create_socket_conn_func.assert_called_once_with()
+ mock_send_handshake_func.assert_called_once_with(
+ -1, snippet_client_v2.ConnectionHandshakeCommand.INIT)
+
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'send_handshake_request')
+ @mock.patch.object(snippet_client_v2.SnippetClientV2,
+ 'create_socket_connection')
+ def test_make_conn_with_forwarded_port_continue(self,
+ mock_create_socket_conn_func,
+ mock_send_handshake_func):
+ """Tests make_connection_with_forwarded_port continues current session."""
+ self._make_client()
+ self.client._counter = None
+ self.client.make_connection_with_forwarded_port(
+ 12345, 54321, 3, snippet_client_v2.ConnectionHandshakeCommand.CONTINUE)
+
+ self.assertEqual(self.client.host_port, 12345)
+ self.assertEqual(self.client.device_port, 54321)
+ self.assertEqual(next(self.client._counter), 0)
+ mock_create_socket_conn_func.assert_called_once_with()
+ mock_send_handshake_func.assert_called_once_with(
+ 3, snippet_client_v2.ConnectionHandshakeCommand.CONTINUE)
if __name__ == '__main__':
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index 6adb8f5..4aa5304 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -601,6 +601,7 @@ class AndroidDeviceTest(unittest.TestCase):
self, sanitize_filename_mock, get_log_file_timestamp_mock, MockFastboot,
MockAdbProxy):
mock_serial = 1
+ sanitize_filename_mock.return_value = '1'
ad = android_device.AndroidDevice(serial=mock_serial)
get_log_file_timestamp_mock.return_value = '07-22-2019_17-53-34-450'
filename = ad.generate_filename('MagicLog')
@@ -1004,7 +1005,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet(self, MockGetPort, MockSnippetClient,
MockFastboot, MockAdbProxy):
@@ -1017,7 +1018,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_getattr(self, MockGetPort, MockSnippetClient,
MockFastboot, MockAdbProxy):
@@ -1032,7 +1033,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient',
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2',
return_value=MockSnippetClient)
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet_dup_package(self, MockGetPort,
@@ -1050,7 +1051,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient',
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2',
return_value=MockSnippetClient)
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet_dup_snippet_name(self, MockGetPort,
@@ -1069,7 +1070,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet_dup_attribute_name(
self, MockGetPort, MockSnippetClient, MockFastboot, MockAdbProxy):
@@ -1084,7 +1085,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet_start_app_fails(self, MockGetPort,
MockSnippetClient,
@@ -1092,14 +1093,13 @@ class AndroidDeviceTest(unittest.TestCase):
MockAdbProxy):
"""Verifies that the correct exception is raised if start app failed.
- It's possible that the `stop_app` call as part of the start app failure
+ It's possible that the `stop` call as part of the start app failure
teardown also fails. So we want the exception from the start app
failure.
"""
expected_e = Exception('start failed.')
- MockSnippetClient.start_app_and_connect = mock.Mock(side_effect=expected_e)
- MockSnippetClient.stop_app = mock.Mock(
- side_effect=Exception('stop failed.'))
+ MockSnippetClient.initialize = mock.Mock(side_effect=expected_e)
+ MockSnippetClient.stop = mock.Mock(side_effect=Exception('stop failed.'))
ad = android_device.AndroidDevice(serial='1')
try:
ad.load_snippet('snippet', MOCK_SNIPPET_PACKAGE_NAME)
@@ -1111,7 +1111,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ '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):
@@ -1132,7 +1132,7 @@ class AndroidDeviceTest(unittest.TestCase):
@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.SnippetClient')
+ 'mobly.controllers.android_device_lib.snippet_client_v2.SnippetClientV2')
@mock.patch('mobly.utils.get_available_host_port')
@mock.patch.object(logcat.Logcat, '_open_logcat_file')
def test_AndroidDevice_snippet_cleanup(self, open_logcat_mock, MockGetPort,
diff --git a/tests/mobly/logger_test.py b/tests/mobly/logger_test.py
index fa514e7..e0ac14d 100755
--- a/tests/mobly/logger_test.py
+++ b/tests/mobly/logger_test.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import logging
import os
import shutil
import tempfile
@@ -56,7 +57,8 @@ class LoggerTest(unittest.TestCase):
mock_create_latest_log_alias,
mock__setup_test_logger):
logger.setup_test_logger(self.log_dir)
- mock__setup_test_logger.assert_called_once_with(self.log_dir, None)
+ mock__setup_test_logger.assert_called_once_with(self.log_dir, logging.INFO,
+ None)
mock_create_latest_log_alias.assert_called_once_with(self.log_dir,
alias='latest')
@@ -208,6 +210,33 @@ class LoggerTest(unittest.TestCase):
expected_filename = 'logcat.txt_'
self.assertEqual(logger.sanitize_filename(fake_filename), expected_filename)
+ def test_prefix_logger_adapter_prefix_log_lines(self):
+ extra = {
+ logger.PrefixLoggerAdapter.EXTRA_KEY_LOG_PREFIX: '[MOCK_PREFIX]',
+ }
+ adapted_logger = logger.PrefixLoggerAdapter(mock.Mock(), extra)
+
+ kwargs = mock.Mock()
+ processed_log, processed_kwargs = adapted_logger.process('mock log line',
+ kwargs=kwargs)
+
+ self.assertEqual(processed_log, '[MOCK_PREFIX] mock log line')
+ self.assertIs(processed_kwargs, kwargs)
+
+ def test_prefix_logger_adapter_modify_prefix(self):
+ extra = {
+ logger.PrefixLoggerAdapter.EXTRA_KEY_LOG_PREFIX: 'MOCK_PREFIX',
+ }
+ adapted_logger = logger.PrefixLoggerAdapter(mock.Mock(), extra)
+ adapted_logger.set_log_prefix('[NEW]')
+
+ kwargs = mock.Mock()
+ processed_log, processed_kwargs = adapted_logger.process('mock log line',
+ kwargs=kwargs)
+
+ self.assertEqual(processed_log, '[NEW] mock log line')
+ self.assertIs(processed_kwargs, kwargs)
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/mobly/records_test.py b/tests/mobly/records_test.py
index f3758f5..ec6254d 100755
--- a/tests/mobly/records_test.py
+++ b/tests/mobly/records_test.py
@@ -36,6 +36,19 @@ class RecordTestError(Exception):
self._something = something
+class RecordTestRecursiveError(Exception):
+ """Error class with self recursion.
+
+ Used for ExceptionRecord tests.
+ """
+
+ def __init__(self):
+ super().__init__(self) # create a self recursion here.
+
+ def __str__(self):
+ return 'Oh ha!'
+
+
class RecordsTest(unittest.TestCase):
"""This test class tests the implementation of classes in mobly.records.
"""
@@ -50,12 +63,19 @@ class RecordsTest(unittest.TestCase):
def tearDown(self):
shutil.rmtree(self.tmp_path)
- def verify_record(self, record, result, details, extras, stacktrace=None):
+ def verify_record(self,
+ record,
+ result,
+ details,
+ extras,
+ termination_signal_type=None,
+ stacktrace=None):
record.update_record()
# Verify each field.
self.assertEqual(record.test_name, self.tn)
self.assertEqual(record.result, result)
self.assertEqual(record.details, details)
+ self.assertEqual(record.termination_signal_type, termination_signal_type)
self.assertEqual(record.extras, extras)
self.assertTrue(record.begin_time, 'begin time should not be empty.')
self.assertTrue(record.end_time, 'end time should not be empty.')
@@ -66,6 +86,8 @@ class RecordsTest(unittest.TestCase):
d[records.TestResultEnums.RECORD_NAME] = self.tn
d[records.TestResultEnums.RECORD_RESULT] = result
d[records.TestResultEnums.RECORD_DETAILS] = details
+ d[records.TestResultEnums.
+ RECORD_TERMINATION_SIGNAL_TYPE] = termination_signal_type
d[records.TestResultEnums.RECORD_EXTRAS] = extras
d[records.TestResultEnums.RECORD_BEGIN_TIME] = record.begin_time
d[records.TestResultEnums.RECORD_END_TIME] = record.end_time
@@ -88,7 +110,7 @@ class RecordsTest(unittest.TestCase):
self.assertTrue(str(record), 'str of the record should not be empty.')
self.assertTrue(repr(record), "the record's repr shouldn't be empty.")
- def test_result_record_pass_none(self):
+ def test_result_record_implicit_pass(self):
record = records.TestResultRecord(self.tn)
record.test_begin()
record.test_pass()
@@ -97,7 +119,7 @@ class RecordsTest(unittest.TestCase):
details=None,
extras=None)
- def test_result_record_pass_with_float_extra(self):
+ def test_result_record_explicit_pass_with_float_extra(self):
record = records.TestResultRecord(self.tn)
record.test_begin()
s = signals.TestPass(self.details, self.float_extra)
@@ -105,9 +127,10 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_PASS,
details=self.details,
+ termination_signal_type='TestPass',
extras=self.float_extra)
- def test_result_record_pass_with_json_extra(self):
+ def test_result_record_explicit_pass_with_json_extra(self):
record = records.TestResultRecord(self.tn)
record.test_begin()
s = signals.TestPass(self.details, self.json_extra)
@@ -115,9 +138,11 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_PASS,
details=self.details,
+ termination_signal_type='TestPass',
extras=self.json_extra)
def test_result_record_fail_none(self):
+ """Verifies that `test_fail` can be called without an error object."""
record = records.TestResultRecord(self.tn)
record.test_begin()
record.test_fail()
@@ -139,6 +164,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_FAIL,
details='Something failed.',
+ termination_signal_type='Exception',
extras=None,
stacktrace='in test_result_record_fail_stacktrace\n '
'raise Exception(\'Something failed.\')\nException: '
@@ -152,6 +178,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_FAIL,
details=self.details,
+ termination_signal_type='TestFailure',
extras=self.float_extra)
def test_result_record_fail_with_unicode_test_signal(self):
@@ -163,6 +190,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_FAIL,
details=details,
+ termination_signal_type='TestFailure',
extras=self.float_extra)
def test_result_record_fail_with_unicode_exception(self):
@@ -174,6 +202,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_FAIL,
details=details,
+ termination_signal_type='Exception',
extras=None)
def test_result_record_fail_with_json_extra(self):
@@ -184,6 +213,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_FAIL,
details=self.details,
+ termination_signal_type='TestFailure',
extras=self.json_extra)
def test_result_record_skip_none(self):
@@ -203,6 +233,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_SKIP,
details=self.details,
+ termination_signal_type='TestSkip',
extras=self.float_extra)
def test_result_record_skip_with_json_extra(self):
@@ -213,6 +244,7 @@ class RecordsTest(unittest.TestCase):
self.verify_record(record=record,
result=records.TestResultEnums.TEST_RESULT_SKIP,
details=self.details,
+ termination_signal_type='TestSkip',
extras=self.json_extra)
def test_result_add_operator_success(self):
@@ -406,6 +438,20 @@ class RecordsTest(unittest.TestCase):
new_er = copy.deepcopy(er)
self.assertIsNot(er, new_er)
self.assertDictEqual(er.to_dict(), new_er.to_dict())
+ self.assertEqual(er.type, 'RecordTestError')
+
+ def test_recursive_exception_record_deepcopy(self):
+ """Makes sure ExceptionRecord wrapper handles deep copy properly in case of
+ recursive exception.
+ """
+ try:
+ raise RecordTestRecursiveError()
+ except RecordTestRecursiveError as e:
+ er = records.ExceptionRecord(e)
+ new_er = copy.deepcopy(er)
+ self.assertIsNot(er, new_er)
+ self.assertDictEqual(er.to_dict(), new_er.to_dict())
+ self.assertEqual(er.type, 'RecordTestRecursiveError')
def test_add_controller_info_record(self):
tr = records.TestResult()
diff --git a/tests/mobly/snippet/callback_event_test.py b/tests/mobly/snippet/callback_event_test.py
new file mode 100755
index 0000000..2593cc3
--- /dev/null
+++ b/tests/mobly/snippet/callback_event_test.py
@@ -0,0 +1,40 @@
+# Copyright 2022 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.
+"""Unit tests for mobly.snippet.callback_event.CallbackEvent."""
+
+import unittest
+
+from mobly.snippet import callback_event
+
+MOCK_CALLBACK_ID = 'myCallbackId'
+MOCK_EVENT_NAME = 'onXyzEvent'
+MOCK_CREATION_TIME = '12345678'
+MOCK_DATA = {'foo': 'bar'}
+
+
+class CallbackEventTest(unittest.TestCase):
+ """Unit tests for mobly.snippet.callback_event.CallbackEvent."""
+
+ def test_basic(self):
+ """Verifies that an event object can be created and logged properly."""
+ event = callback_event.CallbackEvent(MOCK_CALLBACK_ID, MOCK_EVENT_NAME,
+ MOCK_CREATION_TIME, MOCK_DATA)
+ self.assertEqual(
+ repr(event),
+ "CallbackEvent(callback_id: myCallbackId, name: onXyzEvent, "
+ "creation_time: 12345678, data: {'foo': 'bar'})")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/snippet/callback_handler_base_test.py b/tests/mobly/snippet/callback_handler_base_test.py
new file mode 100644
index 0000000..0891fd5
--- /dev/null
+++ b/tests/mobly/snippet/callback_handler_base_test.py
@@ -0,0 +1,183 @@
+# Copyright 2022 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.
+"""Unit tests for mobly.snippet.callback_handler_base.CallbackHandlerBase."""
+
+import unittest
+from unittest import mock
+
+from mobly.snippet import callback_event
+from mobly.snippet import callback_handler_base
+from mobly.snippet import errors
+
+MOCK_CALLBACK_ID = '2-1'
+MOCK_RAW_EVENT = {
+ 'callbackId': '2-1',
+ 'name': 'AsyncTaskResult',
+ 'time': 20460228696,
+ 'data': {
+ 'exampleData': "Here's a simple event.",
+ 'successful': True,
+ 'secretNumber': 12
+ }
+}
+
+
+class FakeCallbackHandler(callback_handler_base.CallbackHandlerBase):
+ """Fake client class for unit tests."""
+
+ def __init__(self,
+ callback_id=None,
+ event_client=None,
+ ret_value=None,
+ method_name=None,
+ device=None,
+ rpc_max_timeout_sec=120,
+ default_timeout_sec=120):
+ """Initializes a fake callback handler object used for unit tests."""
+ super().__init__(callback_id, event_client, ret_value, method_name, device,
+ rpc_max_timeout_sec, default_timeout_sec)
+ self.mock_rpc_func = mock.Mock()
+
+ def callEventWaitAndGetRpc(self, *args, **kwargs):
+ """See base class."""
+ return self.mock_rpc_func.callEventWaitAndGetRpc(*args, **kwargs)
+
+ def callEventGetAllRpc(self, *args, **kwargs):
+ """See base class."""
+ return self.mock_rpc_func.callEventGetAllRpc(*args, **kwargs)
+
+
+class CallbackHandlerBaseTest(unittest.TestCase):
+ """Unit tests for mobly.snippet.callback_handler_base.CallbackHandlerBase."""
+
+ def assert_event_correct(self, actual_event, expected_raw_event_dict):
+ expected_event = callback_event.from_dict(expected_raw_event_dict)
+ self.assertEqual(str(actual_event), str(expected_event))
+
+ def test_default_timeout_too_large(self):
+ err_msg = ('The max timeout of a single RPC must be no smaller than '
+ 'the default timeout of the callback handler. '
+ 'Got rpc_max_timeout_sec=10, default_timeout_sec=20.')
+ with self.assertRaisesRegex(ValueError, err_msg):
+ _ = FakeCallbackHandler(rpc_max_timeout_sec=10, default_timeout_sec=20)
+
+ def test_timeout_property(self):
+ handler = FakeCallbackHandler(rpc_max_timeout_sec=20,
+ default_timeout_sec=10)
+ self.assertEqual(handler.rpc_max_timeout_sec, 20)
+ self.assertEqual(handler.default_timeout_sec, 10)
+ with self.assertRaises(AttributeError):
+ handler.rpc_max_timeout_sec = 5
+
+ with self.assertRaises(AttributeError):
+ handler.default_timeout_sec = 5
+
+ def test_callback_id_property(self):
+ handler = FakeCallbackHandler(callback_id=MOCK_CALLBACK_ID)
+ self.assertEqual(handler.callback_id, MOCK_CALLBACK_ID)
+ with self.assertRaises(AttributeError):
+ handler.callback_id = 'ha'
+
+ def test_event_dict_to_snippet_event(self):
+ handler = FakeCallbackHandler(callback_id=MOCK_CALLBACK_ID)
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+
+ event = handler.waitAndGet('ha', timeout=10)
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+ handler.mock_rpc_func.callEventWaitAndGetRpc.assert_called_once_with(
+ MOCK_CALLBACK_ID, 'ha', 10)
+
+ def test_wait_and_get_timeout_default(self):
+ handler = FakeCallbackHandler(rpc_max_timeout_sec=20, default_timeout_sec=5)
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+ _ = handler.waitAndGet('ha')
+ handler.mock_rpc_func.callEventWaitAndGetRpc.assert_called_once_with(
+ mock.ANY, mock.ANY, 5)
+
+ def test_wait_and_get_timeout_ecxeed_threshold(self):
+ rpc_max_timeout_sec = 5
+ big_timeout_sec = 10
+ handler = FakeCallbackHandler(rpc_max_timeout_sec=rpc_max_timeout_sec,
+ default_timeout_sec=rpc_max_timeout_sec)
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+
+ expected_msg = (
+ f'Specified timeout {big_timeout_sec} is longer than max timeout '
+ f'{rpc_max_timeout_sec}.')
+ with self.assertRaisesRegex(errors.CallbackHandlerBaseError, expected_msg):
+ handler.waitAndGet('ha', big_timeout_sec)
+
+ def test_wait_for_event(self):
+ handler = FakeCallbackHandler()
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+
+ def some_condition(event):
+ return event.data['successful']
+
+ event = handler.waitForEvent('AsyncTaskResult', some_condition, 0.01)
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+
+ def test_wait_for_event_negative(self):
+ handler = FakeCallbackHandler()
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+
+ expected_msg = (
+ 'Timed out after 0.01s waiting for an "AsyncTaskResult" event that'
+ ' satisfies the predicate "some_condition".')
+
+ def some_condition(_):
+ return False
+
+ with self.assertRaisesRegex(errors.CallbackHandlerTimeoutError,
+ expected_msg):
+ handler.waitForEvent('AsyncTaskResult', some_condition, 0.01)
+
+ def test_wait_for_event_max_timeout(self):
+ """waitForEvent should not raise the timeout exceed threshold error."""
+ rpc_max_timeout_sec = 5
+ big_timeout_sec = 10
+ handler = FakeCallbackHandler(rpc_max_timeout_sec=rpc_max_timeout_sec,
+ default_timeout_sec=rpc_max_timeout_sec)
+ handler.mock_rpc_func.callEventWaitAndGetRpc = mock.Mock(
+ return_value=MOCK_RAW_EVENT)
+
+ def some_condition(event):
+ return event.data['successful']
+
+ # This line should not raise.
+ event = handler.waitForEvent('AsyncTaskResult',
+ some_condition,
+ timeout=big_timeout_sec)
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+
+ def test_get_all(self):
+ handler = FakeCallbackHandler(callback_id=MOCK_CALLBACK_ID)
+ handler.mock_rpc_func.callEventGetAllRpc = mock.Mock(
+ return_value=[MOCK_RAW_EVENT, MOCK_RAW_EVENT])
+
+ all_events = handler.getAll('ha')
+ for event in all_events:
+ self.assert_event_correct(event, MOCK_RAW_EVENT)
+
+ handler.mock_rpc_func.callEventGetAllRpc.assert_called_once_with(
+ MOCK_CALLBACK_ID, 'ha')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/suite_runner_test.py b/tests/mobly/suite_runner_test.py
index 7297236..976e7ef 100755
--- a/tests/mobly/suite_runner_test.py
+++ b/tests/mobly/suite_runner_test.py
@@ -12,18 +12,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import inspect
import io
import os
import shutil
+import sys
import tempfile
import unittest
from unittest import mock
+from mobly import base_suite
+from mobly import base_test
+from mobly import config_parser
+from mobly import test_runner
from mobly import suite_runner
from tests.lib import integration2_test
from tests.lib import integration_test
+class FakeTest1(base_test.BaseTestClass):
+ pass
+
+
class SuiteRunnerTest(unittest.TestCase):
def setUp(self):
@@ -108,6 +118,44 @@ class SuiteRunnerTest(unittest.TestCase):
argv=['-c', tmp_file_path])
mock_exit.assert_called_once_with(1)
+ @mock.patch('sys.exit')
+ @mock.patch.object(suite_runner, '_find_suite_class', autospec=True)
+ def test_run_suite_class(self, mock_find_suite_class, mock_exit):
+ mock_called = mock.MagicMock()
+
+ class FakeTestSuite(base_suite.BaseSuite):
+
+ def setup_suite(self, config):
+ mock_called.setup_suite()
+ super().setup_suite(config)
+ self.add_test_class(FakeTest1)
+
+ def teardown_suite(self):
+ mock_called.teardown_suite()
+ super().teardown_suite()
+
+ mock_find_suite_class.return_value = FakeTestSuite
+
+ tmp_file_path = os.path.join(self.tmp_dir, 'config.yml')
+ with io.open(tmp_file_path, 'w', encoding='utf-8') as f:
+ f.write(u"""
+ TestBeds:
+ # A test bed where adb will find Android devices.
+ - Name: SampleTestBed
+ Controllers:
+ MagicDevice: '*'
+ """)
+
+ mock_cli_args = ['test_binary', f'--config={tmp_file_path}']
+
+ with mock.patch.object(sys, 'argv', new=mock_cli_args):
+ suite_runner.run_suite_class()
+
+ mock_find_suite_class.assert_called_once()
+ mock_called.setup_suite.assert_called_once_with()
+ mock_called.teardown_suite.assert_called_once_with()
+ mock_exit.assert_not_called()
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/mobly/test_runner_test.py b/tests/mobly/test_runner_test.py
index 0339c35..efddc4c 100755
--- a/tests/mobly/test_runner_test.py
+++ b/tests/mobly/test_runner_test.py
@@ -17,6 +17,7 @@ import logging
import os
import re
import shutil
+import sys
import tempfile
import unittest
from unittest import mock
@@ -25,12 +26,14 @@ from mobly import config_parser
from mobly import records
from mobly import signals
from mobly import test_runner
+from mobly import utils
from tests.lib import mock_android_device
from tests.lib import mock_controller
from tests.lib import integration_test
from tests.lib import integration2_test
from tests.lib import integration3_test
from tests.lib import multiple_subclasses_module
+from tests.lib import terminated_test
import yaml
@@ -265,6 +268,23 @@ class TestRunnerTest(unittest.TestCase):
self.assertEqual(results['Passed'], 0)
self.assertEqual(results['Failed'], 0)
+ def test_run_when_terminated(self):
+ mock_test_config = self.base_mock_test_config.copy()
+ tr = test_runner.TestRunner(self.log_dir, self.testbed_name)
+ tr.add_test_class(mock_test_config, terminated_test.TerminatedTest)
+
+ with self.assertRaises(signals.TestAbortAll):
+ with self.assertLogs(level=logging.WARNING) as log_output:
+ # Set handler log level due to bug in assertLogs.
+ # https://github.com/python/cpython/issues/86109
+ logging.getLogger().handlers[0].setLevel(logging.WARNING)
+ tr.run()
+
+ self.assertIn('Test received a SIGTERM. Aborting all tests.',
+ log_output.output[0])
+ self.assertIn('Abort all subsequent test classes', log_output.output[1])
+ self.assertIn('Test received a SIGTERM.', log_output.output[1])
+
def test_add_test_class_mismatched_log_path(self):
tr = test_runner.TestRunner('/different/log/dir', self.testbed_name)
with self.assertRaisesRegex(
diff --git a/tests/mobly/utils_test.py b/tests/mobly/utils_test.py
index 16204c9..7e95718 100755
--- a/tests/mobly/utils_test.py
+++ b/tests/mobly/utils_test.py
@@ -223,7 +223,7 @@ class UtilsTest(unittest.TestCase):
self.assertEqual(ret, 0)
def test_run_command_with_timeout_expired(self):
- with self.assertRaises(subprocess.TimeoutExpired):
+ with self.assertRaisesRegex(subprocess.TimeoutExpired, 'sleep'):
_ = utils.run_command(self.sleep_cmd(4), timeout=0.01)
@mock.patch('threading.Timer')