Introduce proper service management for AndroidDevice. (#503)
This is the initial check in of AndroidDevice service mechanism.
More changes and refactorings are coming once the first service (logcat) is in.
* A proper way to manage the life cycles of the AndroidDevice's services.
* Add the service manager into `AndroidDevice`.
* Implement the service for logcat.
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 72f0848..e5cde1c 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -16,7 +16,6 @@
from past.builtins import basestring
import contextlib
-import io
import logging
import os
import shutil
@@ -27,8 +26,10 @@
from mobly import utils
from mobly.controllers.android_device_lib import adb
from mobly.controllers.android_device_lib import fastboot
+from mobly.controllers.android_device_lib import service_manager
from mobly.controllers.android_device_lib import sl4a_client
from mobly.controllers.android_device_lib import snippet_client
+from mobly.controllers.android_device_lib.services import logcat
# Convenience constant for the package of Mobly Bundled Snippets
# (http://github.com/google/mobly-bundled-snippets).
@@ -429,19 +430,6 @@
via fastboot.
"""
- @property
- def _normalized_serial(self):
- """Normalized serial name for usage in log filename.
-
- Some Android emulators use ip:port as their serial names, while on
- Windows `:` is not valid in filename, it should be sanitized first.
- """
- if self._serial is None:
- return None
- normalized_serial = self._serial.replace(' ', '_')
- normalized_serial = normalized_serial.replace(':', '-')
- return normalized_serial
-
def __init__(self, serial=''):
self._serial = str(serial)
# logging.log_path only exists when this is used in an Mobly test run.
@@ -454,12 +442,11 @@
})
self.sl4a = None
self.ed = None
- self._adb_logcat_process = None
- self.adb_logcat_file_path = None
self.adb = adb.AdbProxy(serial)
self.fastboot = fastboot.FastbootProxy(serial)
if not self.is_bootloader and self.is_rootable:
self.root_adb()
+ self.services = service_manager.ServiceManager(self)
# A dict for tracking snippet clients. Keys are clients' attribute
# names, values are the clients: {<attr name string>: <client object>}.
self._snippet_clients = {}
@@ -470,6 +457,26 @@
return '<AndroidDevice|%s>' % self.debug_tag
@property
+ def adb_logcat_file_path(self):
+ try:
+ return self.services.logcat.adb_logcat_file_path
+ except AttributeError:
+ return None
+
+ @property
+ def _normalized_serial(self):
+ """Normalized serial name for usage in log filename.
+
+ Some Android emulators use ip:port as their serial names, while on
+ Windows `:` is not valid in filename, it should be sanitized first.
+ """
+ if self._serial is None:
+ return None
+ normalized_serial = self._serial.replace(' ', '_')
+ normalized_serial = normalized_serial.replace(':', '-')
+ return normalized_serial
+
+ @property
def device_info(self):
"""Information to be pulled into controller info.
@@ -532,7 +539,7 @@
A service can be a snippet or logcat collection.
"""
return any(
- [self._snippet_clients, self._adb_logcat_process, self.sl4a])
+ [self._snippet_clients, self.services.is_any_alive, self.sl4a])
@property
def log_path(self):
@@ -608,11 +615,23 @@
"""Starts long running services on the android device, like adb logcat
capture.
"""
- try:
- self.start_adb_logcat(clear_log)
- except:
- self.log.exception('Failed to start adb logcat!')
- raise
+ configs = logcat.Config(clear_log=clear_log)
+ self.services.register('logcat', logcat.Logcat, configs)
+
+ def start_adb_logcat(self, clear_log=True):
+ """.. deprecated:: 1.8
+
+ Use `self.services.logcat.start` instead.
+ """
+ configs = logcat.Config(clear_log=clear_log)
+ self.services.logcat.start(configs)
+
+ def stop_adb_logcat(self):
+ """.. deprecated:: 1.8
+
+ Use `self.services.logcat.stop` instead.
+ """
+ self.services.logcat.stop()
def stop_services(self):
"""Stops long running services on the Android device.
@@ -633,18 +652,9 @@
client.stop_app()
delattr(self, attr_name)
self._snippet_clients = {}
- self._stop_logcat_process()
+ self.services.unregister_all()
return service_info
- def _stop_logcat_process(self):
- """Stops logcat process."""
- if self._adb_logcat_process:
- try:
- self.stop_adb_logcat()
- except:
- self.log.exception('Failed to stop adb logcat.')
- self._adb_logcat_process = None
-
@contextlib.contextmanager
def handle_reboot(self):
"""Properly manage the service life cycle when the device needs to
@@ -709,16 +719,11 @@
# context
ad.adb.wait_for_device(timeout=SOME_TIMEOUT)
"""
- self._stop_logcat_process()
+ self.services.pause_all()
# Only need to stop dispatcher because it continuously polling device
# It's not necessary to stop snippet and sl4a.
if self.sl4a:
self.sl4a.stop_event_dispatcher()
- # Clears cached adb content, so that the next time start_adb_logcat()
- # won't produce duplicated logs to log file.
- # This helps disconnection that caused by, e.g., USB off; at the
- # cost of losing logs at disconnection caused by reboot.
- self._clear_adb_log()
try:
yield
finally:
@@ -746,9 +751,7 @@
def _reconnect_to_services(self):
"""Reconnects to services after USB reconnected."""
- # Do not clear device log at this time. Otherwise the log during USB
- # disconnection will be lost.
- self.start_services(clear_log=False)
+ self.services.resume_all()
# Restore snippets.
for attr_name, client in self._snippet_clients.items():
client.restore_app_connection()
@@ -817,8 +820,7 @@
model = self.adb.getprop('ro.build.product').lower()
if model == 'sprout':
return model
- else:
- return self.adb.getprop('ro.product.name').lower()
+ return self.adb.getprop('ro.product.name').lower()
def load_config(self, config):
"""Add attributes to the AndroidDevice object based on config.
@@ -938,141 +940,6 @@
# Unpack the 'ed' attribute for compatibility.
self.ed = self.sl4a.ed
- def _is_timestamp_in_range(self, target, begin_time, end_time):
- low = mobly_logger.logline_timestamp_comparator(begin_time,
- target) <= 0
- high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0
- return low and high
-
- def cat_adb_log(self, tag, begin_time):
- """Takes an excerpt of the adb logcat log from a certain time point to
- current time.
-
- Args:
- tag: An identifier of the time period, usualy the name of a test.
- begin_time: Logline format timestamp of the beginning of the time
- period.
- """
- if not self.adb_logcat_file_path:
- raise DeviceError(
- self,
- 'Attempting to cat adb log when none has been collected.')
- end_time = mobly_logger.get_log_line_timestamp()
- self.log.debug('Extracting adb log from logcat.')
- adb_excerpt_path = os.path.join(self.log_path, 'AdbLogExcerpts')
- utils.create_dir(adb_excerpt_path)
- f_name = os.path.basename(self.adb_logcat_file_path)
- out_name = f_name.replace('adblog,', '').replace('.txt', '')
- out_name = ',%s,%s.txt' % (begin_time, out_name)
- out_name = out_name.replace(':', '-')
- tag_len = utils.MAX_FILENAME_LEN - len(out_name)
- tag = tag[:tag_len]
- out_name = tag + out_name
- full_adblog_path = os.path.join(adb_excerpt_path, out_name)
- with io.open(full_adblog_path, 'w', encoding='utf-8') as out:
- in_file = self.adb_logcat_file_path
- with io.open(
- in_file, 'r', encoding='utf-8', errors='replace') as f:
- in_range = False
- while True:
- line = None
- try:
- line = f.readline()
- if not line:
- break
- except:
- continue
- line_time = line[:mobly_logger.log_line_timestamp_len]
- if not mobly_logger.is_valid_logline_timestamp(line_time):
- continue
- if self._is_timestamp_in_range(line_time, begin_time,
- end_time):
- in_range = True
- if not line.endswith('\n'):
- line += '\n'
- out.write(line)
- else:
- if in_range:
- break
-
- def _enable_logpersist(self):
- """Attempts to enable logpersist daemon to persist logs."""
- # Logpersist is only allowed on rootable devices because of excessive
- # reads/writes for persisting logs.
- if not self.is_rootable:
- return
-
- logpersist_warning = ('%s encountered an error enabling persistent'
- ' logs, logs may not get saved.')
- # Android L and older versions do not have logpersist installed,
- # so check that the logpersist scripts exists before trying to use
- # them.
- if not self.adb.has_shell_command('logpersist.start'):
- logging.warning(logpersist_warning, self)
- return
-
- try:
- # Disable adb log spam filter for rootable devices. Have to stop
- # and clear settings first because 'start' doesn't support --clear
- # option before Android N.
- self.adb.shell('logpersist.stop --clear')
- self.adb.shell('logpersist.start')
- except adb.AdbError:
- logging.warning(logpersist_warning, self)
-
- def start_adb_logcat(self, clear_log=True):
- """Starts a standing adb logcat collection in separate subprocesses and
- save the logcat in a file.
-
- This clears the previous cached logcat content on device.
-
- Args:
- clear: If True, clear device log before starting logcat.
- """
- if self._adb_logcat_process:
- raise DeviceError(
- self,
- 'Logcat thread is already running, cannot start another one.')
- if clear_log:
- try:
- self._clear_adb_log()
- except adb.AdbError as e:
- # On Android O, the clear command fails due to a known bug.
- # Catching this so we don't crash from this Android issue.
- if "failed to clear" in e.stderr:
- self.log.warning(
- 'Encountered known Android error to clear logcat.')
- else:
- raise
-
- self._enable_logpersist()
-
- f_name = 'adblog,%s,%s.txt' % (self.model, self._normalized_serial)
- utils.create_dir(self.log_path)
- logcat_file_path = os.path.join(self.log_path, f_name)
- try:
- extra_params = self.adb_logcat_param
- except AttributeError:
- extra_params = ''
- cmd = '"%s" -s %s logcat -v threadtime %s >> "%s"' % (adb.ADB,
- self.serial,
- extra_params,
- logcat_file_path)
- process = utils.start_standing_subprocess(cmd, shell=True)
- self._adb_logcat_process = process
- self.adb_logcat_file_path = logcat_file_path
-
- def stop_adb_logcat(self):
- """Stops the adb logcat collection subprocess.
-
- Raises:
- DeviceError: raised if there's no adb logcat collection going on.
- """
- if not self._adb_logcat_process:
- raise DeviceError(self, 'No ongoing adb logcat collection found.')
- utils.stop_standing_subprocess(self._adb_logcat_process)
- self._adb_logcat_process = None
-
def take_bug_report(self,
test_name,
begin_time,
@@ -1125,10 +992,6 @@
self.log.info('Bugreport for %s taken at %s.', test_name,
full_out_path)
- def _clear_adb_log(self):
- # Clears cached adb content.
- self.adb.logcat('-c')
-
def _terminate_sl4a(self):
"""Terminate the current sl4a session.
diff --git a/mobly/controllers/android_device_lib/errors.py b/mobly/controllers/android_device_lib/errors.py
index c5d889a..077841a 100644
--- a/mobly/controllers/android_device_lib/errors.py
+++ b/mobly/controllers/android_device_lib/errors.py
@@ -16,6 +16,8 @@
from mobly import signals
+HIERARCHY_TOKEN = '::'
+
class Error(signals.ControllerError):
pass
@@ -23,6 +25,26 @@
class DeviceError(Error):
"""Raised for errors specific to an AndroidDevice object."""
+
def __init__(self, ad, msg):
- new_msg = '%s %s' % (repr(ad), msg)
+ template = '%s %s'
+ # If the message starts with the hierarchy token, don't add the extra
+ # space.
+ if isinstance(msg, str) and msg.startswith(HIERARCHY_TOKEN):
+ template = '%s%s'
+ new_msg = template % (repr(ad), msg)
super(DeviceError, self).__init__(new_msg)
+
+
+class ServiceError(DeviceError):
+ """Raised for errors specific to an AndroidDevice service.
+
+ A service is inherently associated with a device instance, so the service
+ error type is a subtype of `DeviceError`.
+ """
+ SERVICE_TYPE = None
+
+ def __init__(self, device, msg):
+ new_msg = '%sService<%s> %s' % (HIERARCHY_TOKEN, self.SERVICE_TYPE,
+ msg)
+ super(ServiceError, self).__init__(device, new_msg)
diff --git a/mobly/controllers/android_device_lib/service_manager.py b/mobly/controllers/android_device_lib/service_manager.py
new file mode 100644
index 0000000..f0a96c4
--- /dev/null
+++ b/mobly/controllers/android_device_lib/service_manager.py
@@ -0,0 +1,112 @@
+# Copyright 2018 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.
+"""Module for the manager of services."""
+# TODO(xpconanfan: move the device errors to a more generic location so
+# other device controllers like iOS can share it.
+from mobly import expects
+from mobly.controllers.android_device_lib import errors
+
+
+class Error(errors.DeviceError):
+ """Root error type for this module."""
+
+
+class ServiceManager(object):
+ """Manager for services of AndroidDevice.
+
+ A service is a long running process that involves an Android device, like
+ adb logcat or Snippet.
+ """
+
+ def __init__(self, device):
+ self._service_objects = {}
+ self._device = device
+
+ @property
+ def is_any_alive(self):
+ """True if any service is alive; False otherwise."""
+ for service in self._service_objects.values():
+ if service.is_alive:
+ return True
+ return False
+
+ def register(self, alias, service_class, configs=None):
+ """Registers a service.
+
+ This will create a service instance, starts the service, and adds the
+ instance to the mananger.
+
+ Args:
+ alias: string, the alias for this instance.
+ service_class: class, the service class to instantiate.
+ configs: (optional) config object to pass to the service class's
+ constructor.
+ """
+ if alias in self._service_objects:
+ raise Error(
+ self._device,
+ 'A service is already registered with alias "%s".' % alias)
+ service_obj = service_class(self._device, configs)
+ service_obj.start()
+ self._service_objects[alias] = service_obj
+
+ def unregister(self, alias):
+ """Unregisters a service instance.
+
+ Stops a service and removes it from the manager.
+
+ Args:
+ alias: string, the alias of the service instance to unregister.
+ """
+ if alias not in self._service_objects:
+ raise Error(self._device,
+ 'No service is registered with alias "%s".' % alias)
+ service_obj = self._service_objects.pop(alias)
+ if service_obj.is_alive:
+ with expects.expect_no_raises(
+ 'Failed to stop service instance "%s".' % alias):
+ service_obj.stop()
+
+ def unregister_all(self):
+ """Safely unregisters all active instances.
+
+ Errors occurred here will be recorded but not raised.
+ """
+ aliases = list(self._service_objects.keys())
+ for alias in aliases:
+ self.unregister(alias)
+
+ def pause_all(self):
+ """Pauses all active service instances."""
+ for alias, obj in self._service_objects.items():
+ if obj.is_alive:
+ with expects.expect_no_raises(
+ 'Failed to pause service "%s".' % alias):
+ obj.pause()
+
+ def resume_all(self):
+ """Resumes all paused service instances."""
+ for alias, obj in self._service_objects.items():
+ if not obj.is_alive:
+ with expects.expect_no_raises(
+ 'Failed to pause service "%s".' % alias):
+ obj.resume()
+
+ def __getattr__(self, name):
+ """Syntactic sugar to enable direct access of service objects by alias.
+
+ Args:
+ name: string, the alias a service object was registered under.
+ """
+ return self._service_objects[name]
diff --git a/mobly/controllers/android_device_lib/services/__init__.py b/mobly/controllers/android_device_lib/services/__init__.py
new file mode 100644
index 0000000..87da62d
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018 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.
diff --git a/mobly/controllers/android_device_lib/services/base_service.py b/mobly/controllers/android_device_lib/services/base_service.py
new file mode 100644
index 0000000..1806ae2
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/base_service.py
@@ -0,0 +1,87 @@
+# Copyright 2018 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.
+"""Module for the BaseService."""
+
+
+class BaseService(object):
+ """Base class of a Mobly AndroidDevice service.
+
+ This class defines the interface for Mobly's AndroidDevice service.
+ """
+
+ def __init__(self, device, configs=None):
+ """Constructor of the class.
+
+ Args:
+ device: the device object this service is associated with.
+ config: optional configuration defined by the author of the service
+ class.
+ """
+ self._device = device
+ self._configs = configs
+
+ @property
+ def is_alive(self):
+ """True if the service is active; False otherwise."""
+ raise NotImplementedError('"is_alive" is a required service property.')
+
+ def start(self, configs=None):
+ """Starts the service.
+
+ Args:
+ configs: optional configs to be passed for startup.
+ """
+ raise NotImplementedError('"start" is a required service method.')
+
+ def stop(self):
+ """Stops the service and cleans up all resources.
+
+ This method should handle any error and not throw.
+ """
+ raise NotImplementedError('"stop" is a required service method.')
+
+ def pause(self):
+ """Pauses a service temporarily.
+
+ For when the Python service object needs to temporarily lose connection
+ to the device without shutting down the service running on the actual
+ device.
+
+ This is relevant when a service needs to maintain a constant connection
+ to the device and the connection is lost if USB connection to the
+ device is disrupted.
+
+ E.g. a services that utilizes a socket connection over adb port
+ forwarding would need to implement this for the situation where the USB
+ connection to the device will be temporarily cut, but the device is not
+ rebooted.
+
+ For more context, see:
+ `mobly.controllers.android_device.AndroidDevice.handle_usb_disconnect`
+
+ If not implemented, we assume the service is not sensitive to device
+ disconnect, and `stop` will be called by default.
+ """
+ self.stop()
+
+ def resume(self):
+ """Resumes a paused service.
+
+ Same context as the `pause` method. This should resume the service
+ after the connection to the device has been re-established.
+
+ If not implemented, we assume the service is not sensitive to device
+ disconnect, and `start` will be called by default.
+ """
+ self.start(configs=self._configs)
diff --git a/mobly/controllers/android_device_lib/services/logcat.py b/mobly/controllers/android_device_lib/services/logcat.py
new file mode 100644
index 0000000..5db5a6c
--- /dev/null
+++ b/mobly/controllers/android_device_lib/services/logcat.py
@@ -0,0 +1,201 @@
+# Copyright 2018 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 copy
+import io
+import logging
+import os
+
+from mobly import logger as mobly_logger
+from mobly import utils
+from mobly.controllers.android_device_lib import adb
+from mobly.controllers.android_device_lib import errors
+from mobly.controllers.android_device_lib.services import base_service
+
+
+class Error(errors.ServiceError):
+ """Root error type for logcat service."""
+ SERVICE_TYPE = 'Logcat'
+
+
+class Config(object):
+ """Config object for logcat service.
+
+ Attributes:
+ clear_log: bool, clears the logcat before collection if True.
+ logcat_params: string, extra params to be added to logcat command.
+ """
+
+ def __init__(self, params=None, clear_log=True):
+ self.clear_log = clear_log
+ self.logcat_params = params if params else ''
+
+
+class Logcat(base_service.BaseService):
+ """Android logcat service for Mobly's AndroidDevice controller."""
+
+ def __init__(self, android_device, configs=None):
+ super(Logcat, self).__init__(android_device, configs)
+ self._ad = android_device
+ self._adb_logcat_process = None
+ self.adb_logcat_file_path = None
+ self._configs = configs if configs else Config()
+
+ def _enable_logpersist(self):
+ """Attempts to enable logpersist daemon to persist logs."""
+ # Logpersist is only allowed on rootable devices because of excessive
+ # reads/writes for persisting logs.
+ if not self._ad.is_rootable:
+ return
+
+ logpersist_warning = ('%s encountered an error enabling persistent'
+ ' logs, logs may not get saved.')
+ # Android L and older versions do not have logpersist installed,
+ # so check that the logpersist scripts exists before trying to use
+ # them.
+ if not self._ad.adb.has_shell_command('logpersist.start'):
+ logging.warning(logpersist_warning, self)
+ return
+
+ try:
+ # Disable adb log spam filter for rootable devices. Have to stop
+ # and clear settings first because 'start' doesn't support --clear
+ # option before Android N.
+ self._ad.adb.shell('logpersist.stop --clear')
+ self._ad.adb.shell('logpersist.start')
+ except adb.AdbError:
+ logging.warning(logpersist_warning, self)
+
+ def _is_timestamp_in_range(self, target, begin_time, end_time):
+ low = mobly_logger.logline_timestamp_comparator(begin_time,
+ target) <= 0
+ high = mobly_logger.logline_timestamp_comparator(end_time, target) >= 0
+ return low and high
+
+ @property
+ def is_alive(self):
+ return True if self._adb_logcat_process else False
+
+ def clear_adb_log(self):
+ # Clears cached adb content.
+ self._ad.adb.logcat('-c')
+
+ def cat_adb_log(self, tag, begin_time):
+ """Takes an excerpt of the adb logcat log from a certain time point to
+ current time.
+
+ Args:
+ tag: An identifier of the time period, usualy the name of a test.
+ begin_time: Logline format timestamp of the beginning of the time
+ period.
+ """
+ if not self.adb_logcat_file_path:
+ raise Error(
+ self._ad,
+ 'Attempting to cat adb log when none has been collected.')
+ end_time = mobly_logger.get_log_line_timestamp()
+ self._ad.log.debug('Extracting adb log from logcat.')
+ adb_excerpt_path = os.path.join(self._ad.log_path, 'AdbLogExcerpts')
+ utils.create_dir(adb_excerpt_path)
+ f_name = os.path.basename(self.adb_logcat_file_path)
+ out_name = f_name.replace('adblog,', '').replace('.txt', '')
+ out_name = ',%s,%s.txt' % (begin_time, out_name)
+ out_name = out_name.replace(':', '-')
+ tag_len = utils.MAX_FILENAME_LEN - len(out_name)
+ tag = tag[:tag_len]
+ out_name = tag + out_name
+ full_adblog_path = os.path.join(adb_excerpt_path, out_name)
+ with io.open(full_adblog_path, 'w', encoding='utf-8') as out:
+ in_file = self.adb_logcat_file_path
+ with io.open(
+ in_file, 'r', encoding='utf-8', errors='replace') as f:
+ in_range = False
+ while True:
+ line = None
+ try:
+ line = f.readline()
+ if not line:
+ break
+ except:
+ continue
+ line_time = line[:mobly_logger.log_line_timestamp_len]
+ if not mobly_logger.is_valid_logline_timestamp(line_time):
+ continue
+ if self._is_timestamp_in_range(line_time, begin_time,
+ end_time):
+ in_range = True
+ if not line.endswith('\n'):
+ line += '\n'
+ out.write(line)
+ else:
+ if in_range:
+ break
+
+ def start(self, configs=None):
+ """Starts a standing adb logcat collection.
+
+ The collection runs in a separate subprocess and saves logs in a file.
+
+ Args:
+ configs: Conifg object.
+ """
+ if self._adb_logcat_process:
+ raise Error(
+ self._ad,
+ 'Logcat thread is already running, cannot start another one.')
+ configs = configs if configs else self._configs
+ if configs.clear_log:
+ self.clear_adb_log()
+
+ self._enable_logpersist()
+
+ f_name = 'adblog,%s,%s.txt' % (self._ad.model,
+ self._ad._normalized_serial)
+ utils.create_dir(self._ad.log_path)
+ logcat_file_path = os.path.join(self._ad.log_path, f_name)
+ cmd = '"%s" -s %s logcat -v threadtime %s >> "%s"' % (
+ adb.ADB, self._ad.serial, configs.logcat_params, logcat_file_path)
+ process = utils.start_standing_subprocess(cmd, shell=True)
+ self._adb_logcat_process = process
+ self.adb_logcat_file_path = logcat_file_path
+
+ def stop(self):
+ """Stops the adb logcat service."""
+ if not self._adb_logcat_process:
+ return
+ try:
+ utils.stop_standing_subprocess(self._adb_logcat_process)
+ except:
+ self._ad.log.exception('Failed to stop adb logcat.')
+ self._adb_logcat_process = None
+
+ def pause(self):
+ """Pauses logcat for usb disconnect."""
+ self.stop()
+ # Clears cached adb content, so that the next time start_adb_logcat()
+ # won't produce duplicated logs to log file.
+ # This helps disconnection that caused by, e.g., USB off; at the
+ # cost of losing logs at disconnection caused by reboot.
+ self.clear_adb_log()
+
+ def resume(self):
+ """Resumes a paused logcat service.
+
+ Args:
+ configs: Not used.
+ """
+ # Do not clear device log at this time. Otherwise the log during USB
+ # disconnection will be lost.
+ resume_configs = copy.copy(self._configs)
+ resume_configs.clear_log = False
+ self.start(resume_configs)
diff --git a/tests/mobly/controllers/android_device_lib/errors_test.py b/tests/mobly/controllers/android_device_lib/errors_test.py
new file mode 100755
index 0000000..d88c254
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/errors_test.py
@@ -0,0 +1,49 @@
+# Copyright 2018 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 android_device_lib.errors."""
+import mock
+
+from future.tests.base import unittest
+
+from mobly.controllers.android_device_lib import errors
+
+
+class ErrorsTest(unittest.TestCase):
+ def test_device_error(self):
+ device = mock.MagicMock()
+ device.__repr__ = lambda _: '[MockDevice]'
+ exception = errors.DeviceError(device, 'Some error message.')
+ self.assertEqual(str(exception), '[MockDevice] Some error message.')
+
+ def test_service_error(self):
+ device = mock.MagicMock()
+ device.__repr__ = lambda _: '[MockDevice]'
+ exception = errors.ServiceError(device, 'Some error message.')
+ self.assertEqual(
+ str(exception), '[MockDevice]::Service<None> Some error message.')
+
+ def test_subclass_service_error(self):
+ class Error(errors.ServiceError):
+ SERVICE_TYPE = 'SomeType'
+
+ device = mock.MagicMock()
+ device.__repr__ = lambda _: '[MockDevice]'
+ exception = Error(device, 'Some error message.')
+ self.assertEqual(
+ str(exception),
+ '[MockDevice]::Service<SomeType> Some error message.')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/service_manager_test.py b/tests/mobly/controllers/android_device_lib/service_manager_test.py
new file mode 100755
index 0000000..1d4c31c
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/service_manager_test.py
@@ -0,0 +1,189 @@
+# Copyright 2018 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's ServiceManager."""
+import mock
+
+from future.tests.base import unittest
+
+from mobly.controllers.android_device_lib import service_manager
+from mobly.controllers.android_device_lib.services import base_service
+
+
+class MockService(base_service.BaseService):
+ def __init__(self, device, configs=None):
+ self._device = device
+ self._configs = configs
+ self._alive = False
+
+ @property
+ def is_alive(self):
+ return self._alive
+
+ def start(self, configs=None):
+ self._alive = True
+ self._device.start()
+
+ def stop(self):
+ self._alive = False
+ self._device.stop()
+
+
+class ServiceManagerTest(unittest.TestCase):
+ def test_service_manager_instantiation(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+
+ def test_register(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service', MockService)
+ service = manager.mock_service
+ self.assertTrue(service)
+ self.assertTrue(service.is_alive)
+ self.assertTrue(manager.is_any_alive)
+
+ def test_register_dup_alias(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service', MockService)
+ msg = '.* A service is already registered with alias "mock_service"'
+ with self.assertRaisesRegex(service_manager.Error, msg):
+ manager.register('mock_service', MockService)
+
+ def test_unregister(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service', MockService)
+ service = manager.mock_service
+ manager.unregister('mock_service')
+ self.assertFalse(manager.is_any_alive)
+ self.assertFalse(service.is_alive)
+
+ def test_unregister_non_existent(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ with self.assertRaisesRegex(
+ service_manager.Error,
+ '.* No service is registered with alias "mock_service"'):
+ manager.unregister('mock_service')
+
+ @mock.patch('mobly.expects.expect_no_raises')
+ def test_unregister_handle_error_from_stop(self, mock_expect_func):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service', MockService)
+ service = manager.mock_service
+ service._device.stop.side_deffect = Exception(
+ 'Something failed in stop.')
+ manager.unregister('mock_service')
+ mock_expect_func.assert_called_once_with(
+ 'Failed to stop service instance "mock_service".')
+
+ def test_unregister_all(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service2 = manager.mock_service2
+ manager.unregister_all()
+ self.assertFalse(manager.is_any_alive)
+ self.assertFalse(service1.is_alive)
+ self.assertFalse(service2.is_alive)
+
+ def test_unregister_all(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service2 = manager.mock_service2
+ manager.unregister_all()
+ self.assertFalse(manager.is_any_alive)
+ self.assertFalse(service1.is_alive)
+ self.assertFalse(service2.is_alive)
+
+ def test_unregister_all_with_some_failed(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service1._device.stop.side_deffect = Exception(
+ 'Something failed in stop.')
+ service2 = manager.mock_service2
+ manager.unregister_all()
+ self.assertFalse(manager.is_any_alive)
+ self.assertFalse(service1.is_alive)
+ self.assertFalse(service2.is_alive)
+
+ def test_pause_all(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service2 = manager.mock_service2
+ manager.pause_all()
+ self.assertFalse(manager.is_any_alive)
+ self.assertFalse(service1.is_alive)
+ self.assertFalse(service2.is_alive)
+
+ def test_pause_all_with_some_failed(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service1._device.pause.side_deffect = Exception(
+ 'Something failed in stop.')
+ service2 = manager.mock_service2
+ manager.pause_all()
+ self.assertFalse(manager.is_any_alive)
+ # state of service1 is undefined
+ # verify state of service2
+ self.assertFalse(service2.is_alive)
+
+ def test_resume_all(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service2 = manager.mock_service2
+ manager.pause_all()
+ manager.resume_all()
+ self.assertTrue(manager.is_any_alive)
+ self.assertTrue(service1.is_alive)
+ self.assertTrue(service2.is_alive)
+
+ def test_resume_all_with_some_failed(self):
+ mock_device = mock.MagicMock()
+ manager = service_manager.ServiceManager(mock_device)
+ manager.register('mock_service1', MockService)
+ manager.register('mock_service2', MockService)
+ service1 = manager.mock_service1
+ service1._device.resume.side_deffect = Exception(
+ 'Something failed in stop.')
+ service2 = manager.mock_service2
+ manager.pause_all()
+ manager.resume_all()
+ self.assertTrue(manager.is_any_alive)
+ # state of service1 is undefined
+ # verify state of service2
+ self.assertTrue(service2.is_alive)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/controllers/android_device_lib/services/__init__.py b/tests/mobly/controllers/android_device_lib/services/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/services/__init__.py
diff --git a/tests/mobly/controllers/android_device_lib/services/logcat_test.py b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
new file mode 100755
index 0000000..52ec2b2
--- /dev/null
+++ b/tests/mobly/controllers/android_device_lib/services/logcat_test.py
@@ -0,0 +1,438 @@
+# Copyright 2018 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 io
+import logging
+import mock
+import os
+import shutil
+import tempfile
+
+from future.tests.base import unittest
+
+from mobly import utils
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+from mobly.controllers.android_device_lib.services import logcat
+
+from tests.lib import mock_android_device
+
+# The expected result of the cat adb operation.
+MOCK_ADB_LOGCAT_CAT_RESULT = [
+ '02-29 14:02:21.456 4454 Something\n',
+ '02-29 14:02:21.789 4454 Something again\n'
+]
+# A mocked piece of adb logcat output.
+MOCK_ADB_LOGCAT = (u'02-29 14:02:19.123 4454 Nothing\n'
+ u'%s'
+ u'02-29 14:02:22.123 4454 Something again and again\n'
+ ) % u''.join(MOCK_ADB_LOGCAT_CAT_RESULT)
+# The expected result of the cat adb operation.
+MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT = [
+ '02-29 14:02:21.456 4454 Something \u901a\n',
+ '02-29 14:02:21.789 4454 Something again\n'
+]
+# A mocked piece of adb logcat output.
+MOCK_ADB_UNICODE_LOGCAT = (
+ u'02-29 14:02:19.123 4454 Nothing\n'
+ u'%s'
+ u'02-29 14:02:22.123 4454 Something again and again\n'
+) % u''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT)
+
+# Mock start and end time of the adb cat.
+MOCK_ADB_LOGCAT_BEGIN_TIME = '02-29 14:02:20.123'
+MOCK_ADB_LOGCAT_END_TIME = '02-29 14:02:22.000'
+
+# Mock AdbError for missing logpersist scripts
+MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR = adb.AdbError(
+ 'logpersist.stop --clear', '',
+ '/system/bin/sh: logpersist.stop: not found', 0)
+MOCK_LOGPERSIST_START_MISSING_ADB_ERROR = adb.AdbError(
+ 'logpersist.start --clear', '',
+ '/system/bin/sh: logpersist.stop: not found', 0)
+
+
+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'):
+ setattr(logging, 'log_path', '/tmp/logs')
+ # Creates a temp dir to be used by tests in this test class.
+ self.tmp_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ """Removes the temp dir.
+ """
+ shutil.rmtree(self.tmp_dir)
+
+ @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')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ def test_logcat_service_start_and_stop(self, stop_proc_mock,
+ start_proc_mock, creat_dir_mock,
+ FastbootProxy, MockAdbProxy):
+ """Verifies the steps of collecting adb logcat on an AndroidDevice
+ object, including various function calls and the expected behaviors of
+ the calls.
+ """
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service.start()
+ # Verify start did the correct operations.
+ self.assertTrue(logcat_service._adb_logcat_process)
+ expected_log_path = os.path.join(logging.log_path,
+ 'AndroidDevice%s' % ad.serial,
+ 'adblog,fakemodel,%s.txt' % ad.serial)
+ creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+ adb_cmd = '"adb" -s %s logcat -v threadtime >> %s'
+ start_proc_mock.assert_called_with(
+ adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+ self.assertEqual(logcat_service.adb_logcat_file_path,
+ expected_log_path)
+ expected_msg = (
+ 'Logcat thread is already running, cannot start another'
+ ' one.')
+ # Expect error if start is called back to back.
+ with self.assertRaisesRegex(logcat.Error, expected_msg):
+ logcat_service.start()
+ # Verify stop did the correct operations.
+ logcat_service.stop()
+ stop_proc_mock.assert_called_with('process')
+ self.assertIsNone(logcat_service._adb_logcat_process)
+ self.assertEqual(logcat_service.adb_logcat_file_path,
+ expected_log_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')
+ @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_pause_and_resume(
+ self, clear_adb_mock, stop_proc_mock, start_proc_mock,
+ creat_dir_mock, FastbootProxy, MockAdbProxy):
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service.start()
+ clear_adb_mock.assert_called_once_with()
+ self.assertTrue(logcat_service.is_alive)
+ logcat_service.pause()
+ self.assertFalse(logcat_service.is_alive)
+ stop_proc_mock.assert_called_with('process')
+ self.assertIsNone(logcat_service._adb_logcat_process)
+ clear_adb_mock.reset_mock()
+ logcat_service.resume()
+ self.assertTrue(logcat_service.is_alive)
+ clear_adb_mock.assert_not_called()
+
+ @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')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ def test_logcat_service_take_logcat_with_extra_params(
+ self, stop_proc_mock, start_proc_mock, creat_dir_mock,
+ FastbootProxy, MockAdbProxy):
+ """Verifies the steps of collecting adb logcat on an AndroidDevice
+ object, including various function calls and the expected behaviors of
+ the calls.
+ """
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ configs = logcat.Config()
+ configs.logcat_params = '-b radio'
+ logcat_service = logcat.Logcat(ad, configs)
+ logcat_service.start()
+ # Verify start did the correct operations.
+ self.assertTrue(logcat_service._adb_logcat_process)
+ expected_log_path = os.path.join(logging.log_path,
+ 'AndroidDevice%s' % ad.serial,
+ 'adblog,fakemodel,%s.txt' % ad.serial)
+ creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+ adb_cmd = '"adb" -s %s logcat -v threadtime -b radio >> %s'
+ start_proc_mock.assert_called_with(
+ adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+ self.assertEqual(logcat_service.adb_logcat_file_path,
+ expected_log_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')
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ def test_logcat_service_take_logcat_with_logcat_params_override_in_start(
+ self, stop_proc_mock, start_proc_mock, creat_dir_mock,
+ FastbootProxy, MockAdbProxy):
+ """Verifies the steps of collecting adb logcat on an AndroidDevice
+ object, including various function calls and the expected behaviors of
+ the calls.
+ """
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ configs = logcat.Config()
+ configs.logcat_params = '-b radio'
+ logcat_service = logcat.Logcat(ad, configs)
+ new_configs = logcat.Config()
+ new_configs.logcat_params = '-b something_else'
+ logcat_service.start(configs=new_configs)
+ # Verify start did the correct operations.
+ self.assertTrue(logcat_service._adb_logcat_process)
+ expected_log_path = os.path.join(logging.log_path,
+ 'AndroidDevice%s' % ad.serial,
+ 'adblog,fakemodel,%s.txt' % ad.serial)
+ creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
+ adb_cmd = '"adb" -s %s logcat -v threadtime -b something_else >> %s'
+ start_proc_mock.assert_called_with(
+ adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
+ self.assertEqual(logcat_service.adb_logcat_file_path,
+ expected_log_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'))
+ def test_logcat_service_instantiation(self, MockFastboot, MockAdbProxy):
+ """Verifies the AndroidDevice object's basic attributes are correctly
+ set after instantiation.
+ """
+ mock_serial = 1
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ self.assertIsNone(logcat_service._adb_logcat_process)
+ self.assertIsNone(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.logger.get_log_line_timestamp',
+ return_value=MOCK_ADB_LOGCAT_END_TIME)
+ def test_logcat_service_cat_adb_log(self, mock_timestamp_getter,
+ stop_proc_mock, start_proc_mock,
+ FastbootProxy, MockAdbProxy):
+ """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
+ file, locates the correct adb log lines within the given time range,
+ and writes the lines to the correct output file.
+ """
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+ # Direct the log path of the ad to a temp dir to avoid racing.
+ logcat_service._ad._log_path = self.tmp_dir
+ # Expect error if attempted to cat adb log before starting adb logcat.
+ expected_msg = ('.* Attempting to cat adb log when none'
+ ' has been collected.')
+ with self.assertRaisesRegex(logcat.Error, expected_msg):
+ logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+ logcat_service.start()
+ utils.create_dir(ad.log_path)
+ mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
+ (ad.model, ad.serial))
+ with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
+ f.write(MOCK_ADB_LOGCAT)
+ logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+ cat_file_path = os.path.join(
+ ad.log_path, 'AdbLogExcerpts',
+ ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
+ with io.open(cat_file_path, 'r', encoding='utf-8') as f:
+ actual_cat = f.read()
+ self.assertEqual(actual_cat, ''.join(MOCK_ADB_LOGCAT_CAT_RESULT))
+ # Stops adb logcat.
+ logcat_service.stop()
+
+ @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.logger.get_log_line_timestamp',
+ return_value=MOCK_ADB_LOGCAT_END_TIME)
+ def test_logcat_service_cat_adb_log_with_unicode(
+ self, mock_timestamp_getter, stop_proc_mock, start_proc_mock,
+ FastbootProxy, MockAdbProxy):
+ """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
+ file, locates the correct adb log lines within the given time range,
+ and writes the lines to the correct output file.
+ """
+ mock_serial = '1'
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+ # Direct the log path of the ad to a temp dir to avoid racing.
+ logcat_service._ad._log_path = self.tmp_dir
+ # Expect error if attempted to cat adb log before starting adb logcat.
+ expected_msg = ('.* Attempting to cat adb log when none'
+ ' has been collected.')
+ with self.assertRaisesRegex(logcat.Error, expected_msg):
+ logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+ logcat_service.start()
+ utils.create_dir(ad.log_path)
+ mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
+ (ad.model, ad.serial))
+ with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
+ f.write(MOCK_ADB_UNICODE_LOGCAT)
+ logcat_service.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
+ cat_file_path = os.path.join(
+ ad.log_path, 'AdbLogExcerpts',
+ ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
+ with io.open(cat_file_path, 'r', encoding='utf-8') as f:
+ actual_cat = f.read()
+ self.assertEqual(actual_cat,
+ ''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT))
+ # Stops adb logcat.
+ logcat_service.stop()
+
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.adb.AdbProxy',
+ return_value=mock.MagicMock())
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+ return_value=mock_android_device.MockFastbootProxy('1'))
+ def test_logcat_service__enable_logpersist_with_logpersist(
+ self, MockFastboot, MockAdbProxy):
+ mock_serial = '1'
+ mock_adb_proxy = MockAdbProxy.return_value
+ # Set getprop to return '1' to indicate the device is rootable.
+ mock_adb_proxy.getprop.return_value = '1'
+ mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+ 'logpersist.start': True,
+ 'logpersist.stop': True, }[command]
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+ mock_adb_proxy.shell.assert_has_calls([
+ mock.call('logpersist.stop --clear'),
+ mock.call('logpersist.start'),
+ ])
+
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.adb.AdbProxy',
+ return_value=mock.MagicMock())
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+ return_value=mock_android_device.MockFastbootProxy('1'))
+ def test_logcat_service__enable_logpersist_with_missing_all_logpersist(
+ self, MockFastboot, MockAdbProxy):
+ def adb_shell_helper(command):
+ if command == 'logpersist.start':
+ raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
+ elif command == 'logpersist.stop --clear':
+ raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
+ else:
+ return ''
+
+ mock_serial = '1'
+ mock_adb_proxy = MockAdbProxy.return_value
+ mock_adb_proxy.getprop.return_value = 'userdebug'
+ mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+ 'logpersist.start': False,
+ 'logpersist.stop': False, }[command]
+ mock_adb_proxy.shell.side_effect = adb_shell_helper
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.adb.AdbProxy',
+ return_value=mock.MagicMock())
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+ return_value=mock_android_device.MockFastbootProxy('1'))
+ def test_logcat_service__enable_logpersist_with_missing_logpersist_stop(
+ self, MockFastboot, MockAdbProxy):
+ def adb_shell_helper(command):
+ if command == 'logpersist.stop --clear':
+ raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
+ else:
+ return ''
+
+ mock_serial = '1'
+ mock_adb_proxy = MockAdbProxy.return_value
+ mock_adb_proxy.getprop.return_value = 'userdebug'
+ mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+ 'logpersist.start': True,
+ 'logpersist.stop': False, }[command]
+ mock_adb_proxy.shell.side_effect = adb_shell_helper
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.adb.AdbProxy',
+ return_value=mock.MagicMock())
+ @mock.patch('mobly.utils.stop_standing_subprocess')
+ def test_logcat_service__enable_logpersist_with_missing_logpersist_start(
+ self, MockFastboot, MockAdbProxy):
+ def adb_shell_helper(command):
+ if command == 'logpersist.start':
+ raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
+ else:
+ return ''
+
+ mock_serial = '1'
+ mock_adb_proxy = MockAdbProxy.return_value
+ mock_adb_proxy.getprop.return_value = 'userdebug'
+ mock_adb_proxy.has_shell_command.side_effect = lambda command: {
+ 'logpersist.start': False,
+ 'logpersist.stop': True, }[command]
+ mock_adb_proxy.shell.side_effect = adb_shell_helper
+ ad = android_device.AndroidDevice(serial=mock_serial)
+ logcat_service = logcat.Logcat(ad)
+ logcat_service._enable_logpersist()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index d3221a4..ffb6a94 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -25,54 +25,20 @@
from future.tests.base import unittest
-from mobly import utils
from mobly.controllers import android_device
from mobly.controllers.android_device_lib import adb
from mobly.controllers.android_device_lib import snippet_client
+from mobly.controllers.android_device_lib.services import base_service
+from mobly.controllers.android_device_lib.services import logcat
from tests.lib import mock_android_device
-# Mock log path for a test run.
-MOCK_LOG_PATH = '/tmp/logs/MockTest/xx-xx-xx_xx-xx-xx/'
-# The expected result of the cat adb operation.
-MOCK_ADB_LOGCAT_CAT_RESULT = [
- '02-29 14:02:21.456 4454 Something\n',
- '02-29 14:02:21.789 4454 Something again\n'
-]
-# A mocked piece of adb logcat output.
-MOCK_ADB_LOGCAT = (u'02-29 14:02:19.123 4454 Nothing\n'
- u'%s'
- u'02-29 14:02:22.123 4454 Something again and again\n'
- ) % u''.join(MOCK_ADB_LOGCAT_CAT_RESULT)
-# The expected result of the cat adb operation.
-MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT = [
- '02-29 14:02:21.456 4454 Something \u901a\n',
- '02-29 14:02:21.789 4454 Something again\n'
-]
-# A mocked piece of adb logcat output.
-MOCK_ADB_UNICODE_LOGCAT = (
- u'02-29 14:02:19.123 4454 Nothing\n'
- u'%s'
- u'02-29 14:02:22.123 4454 Something again and again\n'
-) % u''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT)
-
-# Mock start and end time of the adb cat.
-MOCK_ADB_LOGCAT_BEGIN_TIME = '02-29 14:02:20.123'
-MOCK_ADB_LOGCAT_END_TIME = '02-29 14:02:22.000'
MOCK_SNIPPET_PACKAGE_NAME = 'com.my.snippet'
# A mock SnippetClient used for testing snippet management logic.
MockSnippetClient = mock.MagicMock()
MockSnippetClient.package = MOCK_SNIPPET_PACKAGE_NAME
-# Mock AdbError for missing logpersist scripts
-MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR = adb.AdbError(
- 'logpersist.stop --clear', '',
- '/system/bin/sh: logpersist.stop: not found', 0)
-MOCK_LOGPERSIST_START_MISSING_ADB_ERROR = adb.AdbError(
- 'logpersist.start --clear', '',
- '/system/bin/sh: logpersist.stop: not found', 0)
-
class AndroidDeviceTest(unittest.TestCase):
"""This test class has unit tests for the implementation of everything
@@ -272,8 +238,6 @@
ad = android_device.AndroidDevice(serial=mock_serial)
self.assertEqual(ad.serial, '1')
self.assertEqual(ad.model, 'fakemodel')
- self.assertIsNone(ad._adb_logcat_process)
- self.assertIsNone(ad.adb_logcat_file_path)
expected_lp = os.path.join(logging.log_path,
'AndroidDevice%s' % mock_serial)
self.assertEqual(ad.log_path, expected_lp)
@@ -410,122 +374,6 @@
@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')
- @mock.patch('mobly.utils.stop_standing_subprocess')
- def test_AndroidDevice_take_logcat(self, stop_proc_mock, start_proc_mock,
- creat_dir_mock, FastbootProxy,
- MockAdbProxy):
- """Verifies the steps of collecting adb logcat on an AndroidDevice
- object, including various function calls and the expected behaviors of
- the calls.
- """
- mock_serial = '1'
- ad = android_device.AndroidDevice(serial=mock_serial)
- expected_msg = '.* No ongoing adb logcat collection found.'
- # Expect error if stop is called before start.
- with self.assertRaisesRegex(android_device.Error, expected_msg):
- ad.stop_adb_logcat()
- ad.start_adb_logcat()
- # Verify start did the correct operations.
- self.assertTrue(ad._adb_logcat_process)
- expected_log_path = os.path.join(logging.log_path,
- 'AndroidDevice%s' % ad.serial,
- 'adblog,fakemodel,%s.txt' % ad.serial)
- creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
- adb_cmd = '"adb" -s %s logcat -v threadtime >> %s'
- start_proc_mock.assert_called_with(
- adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
- self.assertEqual(ad.adb_logcat_file_path, expected_log_path)
- expected_msg = (
- 'Logcat thread is already running, cannot start another'
- ' one.')
- # Expect error if start is called back to back.
- with self.assertRaisesRegex(android_device.Error, expected_msg):
- ad.start_adb_logcat()
- # Verify stop did the correct operations.
- ad.stop_adb_logcat()
- stop_proc_mock.assert_called_with('process')
- self.assertIsNone(ad._adb_logcat_process)
- self.assertEqual(ad.adb_logcat_file_path, expected_log_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')
- @mock.patch('mobly.utils.stop_standing_subprocess')
- @mock.patch(
- 'mobly.controllers.android_device.AndroidDevice._clear_adb_log')
- def test_AndroidDevice_start_adb_logcat_clear_log_fails(
- self, mock_clear_adb_log, stop_proc_mock, start_proc_mock,
- creat_dir_mock, FastbootProxy, MockAdbProxy):
- """Verifies the steps of collecting adb logcat on an AndroidDevice
- object, including various function calls and the expected behaviors of
- the calls.
- """
- mock_clear_adb_log.side_effect = adb.AdbError(
- cmd='adb -s xx logcat -c',
- stdout='',
- stderr="failed to clear 'main' log",
- ret_code=1)
- mock_serial = '1'
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad.start_adb_logcat()
- # Verify start did the correct operations.
- self.assertTrue(ad._adb_logcat_process)
- expected_log_path = os.path.join(logging.log_path,
- 'AndroidDevice%s' % ad.serial,
- 'adblog,fakemodel,%s.txt' % ad.serial)
- creat_dir_mock.assert_called_with(os.path.dirname(expected_log_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')
- @mock.patch('mobly.utils.stop_standing_subprocess')
- def test_AndroidDevice_take_logcat_with_user_param(
- self, stop_proc_mock, start_proc_mock, creat_dir_mock,
- FastbootProxy, MockAdbProxy):
- """Verifies the steps of collecting adb logcat on an AndroidDevice
- object, including various function calls and the expected behaviors of
- the calls.
- """
- mock_serial = '1'
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad.adb_logcat_param = '-b radio'
- expected_msg = '.* No ongoing adb logcat collection found.'
- # Expect error if stop is called before start.
- with self.assertRaisesRegex(android_device.Error, expected_msg):
- ad.stop_adb_logcat()
- ad.start_adb_logcat()
- # Verify start did the correct operations.
- self.assertTrue(ad._adb_logcat_process)
- expected_log_path = os.path.join(logging.log_path,
- 'AndroidDevice%s' % ad.serial,
- 'adblog,fakemodel,%s.txt' % ad.serial)
- creat_dir_mock.assert_called_with(os.path.dirname(expected_log_path))
- adb_cmd = '"adb" -s %s logcat -v threadtime -b radio >> %s'
- start_proc_mock.assert_called_with(
- adb_cmd % (ad.serial, '"%s"' % expected_log_path), shell=True)
- self.assertEqual(ad.adb_logcat_file_path, expected_log_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')
@@ -533,8 +381,8 @@
start_proc_mock, FastbootProxy,
MockAdbProxy):
ad = android_device.AndroidDevice(serial='1')
- ad.start_adb_logcat()
- ad.stop_adb_logcat()
+ ad.start_services()
+ ad.services.unregister('logcat')
old_path = ad.log_path
new_log_path = tempfile.mkdtemp()
ad.log_path = new_log_path
@@ -590,7 +438,7 @@
self, stop_proc_mock, start_proc_mock, creat_dir_mock,
FastbootProxy, MockAdbProxy):
ad = android_device.AndroidDevice(serial='1')
- ad.start_adb_logcat()
+ ad.start_services()
new_log_path = tempfile.mkdtemp()
expected_msg = '.* Cannot change `log_path` when there is service running.'
with self.assertRaisesRegex(android_device.Error, expected_msg):
@@ -652,7 +500,7 @@
self, stop_proc_mock, start_proc_mock, creat_dir_mock,
FastbootProxy, MockAdbProxy):
ad = android_device.AndroidDevice(serial='1')
- ad.start_adb_logcat()
+ ad.start_services()
expected_msg = '.* Cannot change device serial number when there is service running.'
with self.assertRaisesRegex(android_device.Error, expected_msg):
ad.update_serial('2')
@@ -664,189 +512,6 @@
'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.logger.get_log_line_timestamp',
- return_value=MOCK_ADB_LOGCAT_END_TIME)
- def test_AndroidDevice_cat_adb_log(self, mock_timestamp_getter,
- stop_proc_mock, start_proc_mock,
- FastbootProxy, MockAdbProxy):
- """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
- file, locates the correct adb log lines within the given time range,
- and writes the lines to the correct output file.
- """
- mock_serial = '1'
- ad = android_device.AndroidDevice(serial=mock_serial)
- # Direct the log path of the ad to a temp dir to avoid racing.
- ad._log_path_base = self.tmp_dir
- # Expect error if attempted to cat adb log before starting adb logcat.
- expected_msg = ('.* Attempting to cat adb log when none'
- ' has been collected.')
- with self.assertRaisesRegex(android_device.Error, expected_msg):
- ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
- ad.start_adb_logcat()
- utils.create_dir(ad.log_path)
- mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
- (ad.model, ad.serial))
- with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
- f.write(MOCK_ADB_LOGCAT)
- ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
- cat_file_path = os.path.join(
- ad.log_path, 'AdbLogExcerpts',
- ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
- with io.open(cat_file_path, 'r', encoding='utf-8') as f:
- actual_cat = f.read()
- self.assertEqual(actual_cat, ''.join(MOCK_ADB_LOGCAT_CAT_RESULT))
- # Stops adb logcat.
- ad.stop_adb_logcat()
-
- @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.logger.get_log_line_timestamp',
- return_value=MOCK_ADB_LOGCAT_END_TIME)
- def test_AndroidDevice_cat_adb_log_with_unicode(
- self, mock_timestamp_getter, stop_proc_mock, start_proc_mock,
- FastbootProxy, MockAdbProxy):
- """Verifies that AndroidDevice.cat_adb_log loads the correct adb log
- file, locates the correct adb log lines within the given time range,
- and writes the lines to the correct output file.
- """
- mock_serial = '1'
- ad = android_device.AndroidDevice(serial=mock_serial)
- # Direct the log path of the ad to a temp dir to avoid racing.
- ad._log_path_base = self.tmp_dir
- # Expect error if attempted to cat adb log before starting adb logcat.
- expected_msg = ('.* Attempting to cat adb log when none'
- ' has been collected.')
- with self.assertRaisesRegex(android_device.Error, expected_msg):
- ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
- ad.start_adb_logcat()
- utils.create_dir(ad.log_path)
- mock_adb_log_path = os.path.join(ad.log_path, 'adblog,%s,%s.txt' %
- (ad.model, ad.serial))
- with io.open(mock_adb_log_path, 'w', encoding='utf-8') as f:
- f.write(MOCK_ADB_UNICODE_LOGCAT)
- ad.cat_adb_log('some_test', MOCK_ADB_LOGCAT_BEGIN_TIME)
- cat_file_path = os.path.join(
- ad.log_path, 'AdbLogExcerpts',
- ('some_test,02-29 14-02-20.123,%s,%s.txt') % (ad.model, ad.serial))
- with io.open(cat_file_path, 'r', encoding='utf-8') as f:
- actual_cat = f.read()
- self.assertEqual(actual_cat,
- ''.join(MOCK_ADB_UNICODE_LOGCAT_CAT_RESULT))
- # Stops adb logcat.
- ad.stop_adb_logcat()
-
- @mock.patch(
- 'mobly.controllers.android_device_lib.adb.AdbProxy',
- return_value=mock.MagicMock())
- @mock.patch(
- 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
- return_value=mock_android_device.MockFastbootProxy('1'))
- def test_AndroidDevice__enable_logpersist_with_logpersist(
- self, MockFastboot, MockAdbProxy):
- mock_serial = '1'
- mock_adb_proxy = MockAdbProxy.return_value
- # Set getprop to return '1' to indicate the device is rootable.
- mock_adb_proxy.getprop.return_value = '1'
- mock_adb_proxy.has_shell_command.side_effect = lambda command: {
- 'logpersist.start': True,
- 'logpersist.stop': True, }[command]
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad._enable_logpersist()
- mock_adb_proxy.shell.assert_has_calls([
- mock.call('logpersist.stop --clear'),
- mock.call('logpersist.start'),
- ])
-
- @mock.patch(
- 'mobly.controllers.android_device_lib.adb.AdbProxy',
- return_value=mock.MagicMock())
- @mock.patch(
- 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
- return_value=mock_android_device.MockFastbootProxy('1'))
- def test_AndroidDevice__enable_logpersist_with_missing_all_logpersist(
- self, MockFastboot, MockAdbProxy):
- def adb_shell_helper(command):
- if command == 'logpersist.start':
- raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
- elif command == 'logpersist.stop --clear':
- raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
- else:
- return ''
-
- mock_serial = '1'
- mock_adb_proxy = MockAdbProxy.return_value
- mock_adb_proxy.getprop.return_value = 'userdebug'
- mock_adb_proxy.has_shell_command.side_effect = lambda command: {
- 'logpersist.start': False,
- 'logpersist.stop': False, }[command]
- mock_adb_proxy.shell.side_effect = adb_shell_helper
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad._enable_logpersist()
-
- @mock.patch(
- 'mobly.controllers.android_device_lib.adb.AdbProxy',
- return_value=mock.MagicMock())
- @mock.patch(
- 'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
- return_value=mock_android_device.MockFastbootProxy('1'))
- def test_AndroidDevice__enable_logpersist_with_missing_logpersist_stop(
- self, MockFastboot, MockAdbProxy):
- def adb_shell_helper(command):
- if command == 'logpersist.stop --clear':
- raise MOCK_LOGPERSIST_STOP_MISSING_ADB_ERROR
- else:
- return ''
-
- mock_serial = '1'
- mock_adb_proxy = MockAdbProxy.return_value
- mock_adb_proxy.getprop.return_value = 'userdebug'
- mock_adb_proxy.has_shell_command.side_effect = lambda command: {
- 'logpersist.start': True,
- 'logpersist.stop': False, }[command]
- mock_adb_proxy.shell.side_effect = adb_shell_helper
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad._enable_logpersist()
-
- @mock.patch(
- 'mobly.controllers.android_device_lib.adb.AdbProxy',
- return_value=mock.MagicMock())
- @mock.patch('mobly.utils.stop_standing_subprocess')
- def test_AndroidDevice__enable_logpersist_with_missing_logpersist_start(
- self, MockFastboot, MockAdbProxy):
- def adb_shell_helper(command):
- if command == 'logpersist.start':
- raise MOCK_LOGPERSIST_START_MISSING_ADB_ERROR
- else:
- return ''
-
- mock_serial = '1'
- mock_adb_proxy = MockAdbProxy.return_value
- mock_adb_proxy.getprop.return_value = 'userdebug'
- mock_adb_proxy.has_shell_command.side_effect = lambda command: {
- 'logpersist.start': False,
- 'logpersist.stop': True, }[command]
- mock_adb_proxy.shell.side_effect = adb_shell_helper
- ad = android_device.AndroidDevice(serial=mock_serial)
- ad._enable_logpersist()
-
- @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.controllers.android_device_lib.snippet_client.SnippetClient')
@mock.patch('mobly.utils.get_available_host_port')
def test_AndroidDevice_load_snippet(self, MockGetPort, MockSnippetClient,
@@ -1037,6 +702,7 @@
def test_AndroidDevice_snippet_cleanup(
self, MockGetPort, MockSnippetClient, MockFastboot, MockAdbProxy):
ad = android_device.AndroidDevice(serial='1')
+ ad.start_services()
ad.load_snippet('snippet', MOCK_SNIPPET_PACKAGE_NAME)
ad.stop_services()
self.assertFalse(hasattr(ad, 'snippet'))
@@ -1067,6 +733,52 @@
except Exception as e:
self.assertEqual("(<AndroidDevice|Mememe>, 'Something')", str(e))
+ @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')
+ def test_AndroidDevice_handle_usb_disconnect(self, stop_proc_mock,
+ start_proc_mock,
+ FastbootProxy, MockAdbProxy):
+ class MockService(base_service.BaseService):
+ def __init__(self, device, configs=None):
+ self._alive = False
+ self.pause_called = False
+ self.resume_called = False
+
+ @property
+ def is_alive(self):
+ return self._alive
+
+ def start(self, configs=None):
+ self._alive = True
+
+ def stop(self):
+ self._alive = False
+
+ def pause(self):
+ self._alive = False
+ self.pause_called = True
+
+ def resume(self):
+ self._alive = True
+ self.resume_called = True
+
+ ad = android_device.AndroidDevice(serial='1')
+ ad.start_services()
+ ad.services.register('mock_service', MockService)
+ with ad.handle_usb_disconnect():
+ self.assertFalse(ad.services.is_any_alive)
+ self.assertTrue(ad.services.mock_service.pause_called)
+ self.assertFalse(ad.services.mock_service.resume_called)
+ self.assertTrue(ad.services.is_any_alive)
+ self.assertTrue(ad.services.mock_service.resume_called)
+
if __name__ == '__main__':
unittest.main()