Skip to content

Commit

Permalink
Standardize output excerpt APIs in services. (#633)
Browse files Browse the repository at this point in the history
  • Loading branch information
xpconanfan authored Sep 26, 2019
1 parent f596d09 commit e6888c8
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 3 deletions.
37 changes: 37 additions & 0 deletions mobly/controllers/android_device_lib/service_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions mobly/controllers/android_device_lib/services/base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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."""
Expand Down Expand Up @@ -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 []
29 changes: 26 additions & 3 deletions mobly/controllers/android_device_lib/services/logcat.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,44 @@ 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`.
This moves the current content of `self.adb_logcat_file_path` to the
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):
Expand Down
62 changes: 62 additions & 0 deletions tests/mobly/controllers/android_device_lib/service_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
53 changes: 53 additions & 0 deletions tests/mobly/controllers/android_device_lib/services/logcat_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit e6888c8

Please sign in to comment.