Standardize output excerpt APIs in services. (#633)
diff --git a/mobly/controllers/android_device_lib/service_manager.py b/mobly/controllers/android_device_lib/service_manager.py
index ef6ac4e..11b0ab8 100644
--- a/mobly/controllers/android_device_lib/service_manager.py
+++ b/mobly/controllers/android_device_lib/service_manager.py
@@ -82,6 +82,7 @@
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 @@
'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 14380df..c785db2 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.
@@ -36,6 +38,18 @@
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."""
raise NotImplementedError('"is_alive" is a required service property.')
@@ -85,3 +99,29 @@
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 6671fbb..a27fc40 100644
--- a/mobly/controllers/android_device_lib/services/logcat.py
+++ b/mobly/controllers/android_device_lib/services/logcat.py
@@ -100,6 +100,9 @@
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 @@
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 e7a3932..6e24505 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 @@
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 @@
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 0000000..e97848d
--- /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 3106a02..e4d2ca6 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'):
@@ -263,6 +264,58 @@
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',
+ return_value=mock_android_device.MockFastbootProxy('1'))
@mock.patch('mobly.utils.create_dir')
@mock.patch('mobly.utils.start_standing_subprocess',
return_value='process')