From e6888c85b498c39b640214c5c4fe534e720eb558 Mon Sep 17 00:00:00 2001 From: Ang Li Date: Thu, 26 Sep 2019 16:04:21 -0700 Subject: [PATCH] Standardize output excerpt APIs in services. (#633) --- .../android_device_lib/service_manager.py | 37 +++++++++++ .../services/base_service.py | 40 ++++++++++++ .../android_device_lib/services/logcat.py | 29 ++++++++- .../service_manager_test.py | 62 +++++++++++++++++++ .../services/base_service_test.py | 31 ++++++++++ .../services/logcat_test.py | 53 ++++++++++++++++ 6 files changed, 249 insertions(+), 3 deletions(-) create mode 100755 tests/mobly/controllers/android_device_lib/services/base_service_test.py diff --git a/mobly/controllers/android_device_lib/service_manager.py b/mobly/controllers/android_device_lib/service_manager.py index ef6ac4ee..11b0ab81 100644 --- a/mobly/controllers/android_device_lib/service_manager.py +++ b/mobly/controllers/android_device_lib/service_manager.py @@ -82,6 +82,7 @@ def register(self, alias, service_class, configs=None, start_service=True): self._device, 'A service is already registered with alias "%s".' % alias) service_obj = service_class(self._device, configs) + service_obj.alias = alias if start_service: service_obj.start() self._service_objects[alias] = service_obj @@ -103,6 +104,42 @@ def unregister(self, alias): 'Failed to stop service instance "%s".' % alias): service_obj.stop() + def for_each(self, func): + """Executes a function with all registered services. + + Args: + func: function, the function to execute. This function should take + a service object as args. + """ + aliases = list(self._service_objects.keys()) + for alias in aliases: + with expects.expect_no_raises( + 'Failed to execute "%s" for service "%s".' % + (func.__name__, alias)): + func(self._service_objects[alias]) + + def create_output_excerpts_all(self, test_info): + """Creates output excerpts from all services. + + This calls `create_output_excerpts` on all registered services. + + Args: + test_info: RuntimeTestInfo, the test info associated with the scope + of the excerpts. + + Returns: + Dict, keys are the names of the services, values are the paths to + the excerpt files created by the corresponding services. + """ + excerpt_paths = {} + + def create_output_excerpts_for_one(service): + paths = service.create_output_excerpts(test_info) + excerpt_paths[service.alias] = paths + + self.for_each(create_output_excerpts_for_one) + return excerpt_paths + def unregister_all(self): """Safely unregisters all active instances. diff --git a/mobly/controllers/android_device_lib/services/base_service.py b/mobly/controllers/android_device_lib/services/base_service.py index 14380df8..c785db25 100644 --- a/mobly/controllers/android_device_lib/services/base_service.py +++ b/mobly/controllers/android_device_lib/services/base_service.py @@ -14,11 +14,13 @@ """Module for the BaseService.""" +#TODO(xpconanfan): use `abc` after py2 deprecation. class BaseService(object): """Base class of a Mobly AndroidDevice service. This class defines the interface for Mobly's AndroidDevice service. """ + _alias = None def __init__(self, device, configs=None): """Constructor of the class. @@ -35,6 +37,18 @@ def __init__(self, device, configs=None): self._device = device self._configs = configs + @property + def alias(self): + """String, alias used to register this service with service manager. + + This can be None if the service is never registered. + """ + return self._alias + + @alias.setter + def alias(self, alias): + self._alias = alias + @property def is_alive(self): """True if the service is active; False otherwise.""" @@ -85,3 +99,29 @@ def resume(self): disconnect, and `start` will be called by default. """ self.start() + + def create_output_excerpts(self, test_info): + """Creates excerpts of the service's output files. + + [Optional] This method only applies to services with output files. + + For services that generates output files, calling this method would + create excerpts of the output files. An excerpt should contain info + between two calls of `create_output_excerpts` or from the start of the + service to the call to `create_output_excerpts`. + + Use `AndroidDevice#generate_filename` to get the proper filenames for + excerpts. + + This is usually called at the end of: `setup_class`, `teardown_test`, + or `teardown_class`. + + Args: + test_info: RuntimeTestInfo, the test info associated with the scope + of the excerpts. + + Returns: + List of strings, the absolute paths to the excerpt files created. + Empty list if no excerpt files are created. + """ + return [] diff --git a/mobly/controllers/android_device_lib/services/logcat.py b/mobly/controllers/android_device_lib/services/logcat.py index 6671fbb3..a27fc404 100644 --- a/mobly/controllers/android_device_lib/services/logcat.py +++ b/mobly/controllers/android_device_lib/services/logcat.py @@ -100,6 +100,9 @@ def _is_timestamp_in_range(self, target, begin_time, end_time): def create_per_test_excerpt(self, current_test_info): """Convenient method for creating excerpts of adb logcat. + .. deprecated:: 1.9.2 + Use :func:`create_output_excerpts` instead. + To use this feature, call this method at the end of: `setup_class`, `teardown_test`, and `teardown_class`. @@ -107,14 +110,34 @@ def create_per_test_excerpt(self, current_test_info): log directory specific to the current test. Args: - current_test_info: `self.current_test_info` in a Mobly test. + current_test_info: `self.current_test_info` in a Mobly test. + """ + self.create_output_excerpts(current_test_info) + + def create_output_excerpts(self, test_info): + """Convenient method for creating excerpts of adb logcat. + + This moves the current content of `self.adb_logcat_file_path` to the + log directory specific to the current test. + + Call this method at the end of: `setup_class`, `teardown_test`, and + `teardown_class`. + + Args: + test_info: `self.current_test_info` in a Mobly test. + + Returns: + List of strings, the absolute paths to excerpt files. """ self.pause() - dest_path = current_test_info.output_path + dest_path = test_info.output_path utils.create_dir(dest_path) - self._ad.log.debug('AdbLog excerpt location: %s', dest_path) + filename = os.path.basename(self.adb_logcat_file_path) shutil.move(self.adb_logcat_file_path, dest_path) self.resume() + excerpt_file_path = os.path.join(dest_path, filename) + self._ad.log.debug('AdbLog excerpt created at: %s', excerpt_file_path) + return [excerpt_file_path] @property def is_alive(self): diff --git a/tests/mobly/controllers/android_device_lib/service_manager_test.py b/tests/mobly/controllers/android_device_lib/service_manager_test.py index e7a39323..6e24505f 100755 --- a/tests/mobly/controllers/android_device_lib/service_manager_test.py +++ b/tests/mobly/controllers/android_device_lib/service_manager_test.py @@ -76,6 +76,7 @@ def test_register(self): self.assertTrue(service) self.assertTrue(service.is_alive) self.assertTrue(manager.is_any_alive) + self.assertEqual(service.alias, 'mock_service') self.assertEqual(service.start_func.call_count, 1) def test_register_with_configs(self): @@ -118,6 +119,67 @@ def test_register_dup_alias(self): with self.assertRaisesRegex(service_manager.Error, msg): manager.register('mock_service', MockService) + def test_for_each(self): + manager = service_manager.ServiceManager(mock.MagicMock()) + manager.register('mock_service1', MockService) + manager.register('mock_service2', MockService) + service1 = manager.mock_service1 + service2 = manager.mock_service2 + service1.ha = mock.MagicMock() + service2.ha = mock.MagicMock() + manager.for_each(lambda service: service.ha()) + service1.ha.assert_called_with() + service2.ha.assert_called_with() + + def test_for_each_modify_during_iteration(self): + manager = service_manager.ServiceManager(mock.MagicMock()) + manager.register('mock_service1', MockService) + manager.register('mock_service2', MockService) + service1 = manager.mock_service1 + service2 = manager.mock_service2 + service1.ha = mock.MagicMock() + service2.ha = mock.MagicMock() + manager.for_each(lambda service: manager._service_objects.pop(service. + alias)) + self.assertFalse(manager._service_objects) + + def test_for_each_one_fail(self): + manager = service_manager.ServiceManager(mock.MagicMock()) + manager.register('mock_service1', MockService) + manager.register('mock_service2', MockService) + service1 = manager.mock_service1 + service2 = manager.mock_service2 + service1.ha = mock.MagicMock() + service1.ha.side_effect = Exception('Failure in service1.') + service2.ha = mock.MagicMock() + manager.for_each(lambda service: service.ha()) + service1.ha.assert_called_with() + service2.ha.assert_called_with() + self.assert_recorded_one_error('Failure in service1.') + + def test_create_output_excerpts_all(self): + manager = service_manager.ServiceManager(mock.MagicMock()) + manager.register('mock_service1', MockService) + manager.register('mock_service2', MockService) + manager.register('mock_service3', MockService) + service1 = manager.mock_service1 + service2 = manager.mock_service2 + service3 = manager.mock_service3 + service1.create_output_excerpts = mock.MagicMock() + service2.create_output_excerpts = mock.MagicMock() + service3.create_output_excerpts = mock.MagicMock() + service1.create_output_excerpts.return_value = ['path/to/1.txt'] + service2.create_output_excerpts.return_value = [ + 'path/to/2-1.txt', 'path/to/2-2.txt' + ] + service3.create_output_excerpts.return_value = [] + mock_test_info = mock.MagicMock(output_path='path/to') + result = manager.create_output_excerpts_all(mock_test_info) + self.assertEqual(result['mock_service1'], ['path/to/1.txt']) + self.assertEqual(result['mock_service2'], + ['path/to/2-1.txt', 'path/to/2-2.txt']) + self.assertEqual(result['mock_service3'], []) + def test_unregister(self): manager = service_manager.ServiceManager(mock.MagicMock()) manager.register('mock_service', MockService) diff --git a/tests/mobly/controllers/android_device_lib/services/base_service_test.py b/tests/mobly/controllers/android_device_lib/services/base_service_test.py new file mode 100755 index 00000000..e97848d7 --- /dev/null +++ b/tests/mobly/controllers/android_device_lib/services/base_service_test.py @@ -0,0 +1,31 @@ +# Copyright 2019 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 mock +from future.tests.base import unittest +from mobly.controllers.android_device_lib.services import base_service + + +class BaseServiceTest(unittest.TestCase): + def setUp(self): + self.mock_device = mock.MagicMock() + self.service = base_service.BaseService(self.mock_device) + + def test_alias(self): + self.service.alias = 'SomeService' + self.assertEqual(self.service.alias, 'SomeService') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mobly/controllers/android_device_lib/services/logcat_test.py b/tests/mobly/controllers/android_device_lib/services/logcat_test.py index 3106a027..e4d2ca60 100755 --- a/tests/mobly/controllers/android_device_lib/services/logcat_test.py +++ b/tests/mobly/controllers/android_device_lib/services/logcat_test.py @@ -66,6 +66,7 @@ class LogcatTest(unittest.TestCase): """Tests for Logcat service and its integration with AndroidDevice.""" + def setUp(self): # Set log_path to logging since mobly logger setup is not called. if not hasattr(logging, 'log_path'): @@ -259,6 +260,58 @@ def test_logcat_service_create_excerpt(self, clear_adb_mock, self.AssertFileDoesNotContain(FILE_CONTENT, expected_path1) self.assertFalse(os.path.exists(logcat_service.adb_logcat_file_path)) + @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy', + return_value=mock_android_device.MockAdbProxy('1')) + @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy', + return_value=mock_android_device.MockFastbootProxy('1')) + @mock.patch('mobly.utils.start_standing_subprocess', + return_value='process') + @mock.patch('mobly.utils.stop_standing_subprocess') + @mock.patch( + 'mobly.controllers.android_device_lib.services.logcat.Logcat.clear_adb_log', + return_value=mock_android_device.MockAdbProxy('1')) + def test_logcat_service_create_output_excerpts(self, clear_adb_mock, + stop_proc_mock, + start_proc_mock, + FastbootProxy, + MockAdbProxy): + mock_serial = '1' + ad = android_device.AndroidDevice(serial=mock_serial) + logcat_service = logcat.Logcat(ad) + logcat_service.start() + FILE_CONTENT = 'Some log.\n' + with open(logcat_service.adb_logcat_file_path, 'w') as f: + f.write(FILE_CONTENT) + test_output_dir = os.path.join(self.tmp_dir, 'test_foo') + mock_record = mock.MagicMock() + mock_record.begin_time = 123 + test_run_info = runtime_test_info.RuntimeTestInfo( + 'test_foo', test_output_dir, mock_record) + actual_path1 = logcat_service.create_output_excerpts(test_run_info)[0] + expected_path1 = os.path.join(test_output_dir, 'test_foo-123', + 'adblog,fakemodel,1.txt') + self.assertTrue(os.path.exists(expected_path1)) + self.assertEqual(actual_path1, expected_path1) + self.AssertFileContains(FILE_CONTENT, expected_path1) + self.assertFalse(os.path.exists(logcat_service.adb_logcat_file_path)) + # Generate some new logs and do another excerpt. + FILE_CONTENT = 'Some more logs!!!\n' + with open(logcat_service.adb_logcat_file_path, 'w') as f: + f.write(FILE_CONTENT) + test_output_dir = os.path.join(self.tmp_dir, 'test_bar') + mock_record = mock.MagicMock() + mock_record.begin_time = 456 + test_run_info = runtime_test_info.RuntimeTestInfo( + 'test_bar', test_output_dir, mock_record) + actual_path2 = logcat_service.create_output_excerpts(test_run_info)[0] + expected_path2 = os.path.join(test_output_dir, 'test_bar-456', + 'adblog,fakemodel,1.txt') + self.assertTrue(os.path.exists(expected_path2)) + self.assertEqual(actual_path2, expected_path2) + self.AssertFileContains(FILE_CONTENT, expected_path2) + self.AssertFileDoesNotContain(FILE_CONTENT, expected_path1) + self.assertFalse(os.path.exists(logcat_service.adb_logcat_file_path)) + @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy', return_value=mock_android_device.MockAdbProxy('1')) @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',