Cache build info (#600)
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index 75d8cf3..a9dfe98 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -44,6 +44,18 @@
ANDROID_DEVICE_EMPTY_CONFIG_MSG = 'Configuration is empty, abort!'
ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = 'Configuration should be a list, abort!'
+# System properties that are cached by the `AndroidDevice.build_info` property.
+# The only properties on this list should be read-only system properties.
+CACHED_SYSTEM_PROPS = [
+ 'ro.build.id',
+ 'ro.build.type',
+ 'ro.build.version.codename',
+ 'ro.build.version.sdk',
+ 'ro.build.product',
+ 'ro.debuggable',
+ 'ro.product.name',
+]
+
# Keys for attributes in configs that alternate the controller module behavior.
# If this is False for a device, errors from that device will be ignored
# during `create`. Default is True.
@@ -440,6 +452,8 @@
self.log = AndroidDeviceLoggerAdapter(logging.getLogger(), {
'tag': self.debug_tag
})
+ self._build_info = None
+ self._is_rebooting = False
self.adb = adb.AdbProxy(serial)
self.fastboot = fastboot.FastbootProxy(serial)
if not self.is_bootloader and self.is_rootable:
@@ -639,10 +653,30 @@
For sample usage, see self.reboot().
"""
self.services.stop_all()
+ # On rooted devices, system properties may change on reboot, so disable
+ # the `build_info` cache by setting `_is_rebooting` to True and
+ # repopulate it after reboot.
+ # Note, this logic assumes that instance variable assignment in Python
+ # is atomic; otherwise, `threading` data structures would be necessary.
+ # Additionally, nesting calls to `handle_reboot` while changing the
+ # read-only property values during reboot will result in stale values.
+ self._is_rebooting = True
try:
yield
finally:
self.wait_for_boot_completion()
+ # On boot completion, invalidate the `build_info` cache since any
+ # value it had from before boot completion is potentially invalid.
+ # If the value gets set after the final invalidation and before
+ # setting`_is_rebooting` to True, then that's okay because the
+ # device has finished rebooting at that point, and values at that
+ # point should be valid.
+ # If the reboot fails for some reason, then `_is_rebooting` is never
+ # set to False, which means the `build_info` cache remains disabled
+ # until the next reboot. This is relatively okay because the
+ # `build_info` cache is only minimizes adb commands.
+ self._build_info = None
+ self._is_rebooting = False
if self.is_rootable:
self.root_adb()
self.services.start_all()
@@ -715,25 +749,21 @@
self.log.error('Device is in fastboot mode, could not get build '
'info.')
return
- info = {}
- build_info = self.adb.getprops([
- 'ro.build.id',
- 'ro.build.type',
- 'ro.build.version.codename',
- 'ro.build.version.sdk',
- 'ro.build.product',
- 'ro.debuggable',
- 'ro.product.name',
- ])
- info['build_id'] = build_info['ro.build.id']
- info['build_type'] = build_info['ro.build.type']
- info['build_version_codename'] = build_info.get(
- 'ro.build.version.codename', '')
- info['build_version_sdk'] = build_info.get('ro.build.version.sdk', '')
- info['build_product'] = build_info.get('ro.build.product', '')
- info['debuggable'] = build_info.get('ro.debuggable', '')
- info['product_name'] = build_info.get('ro.product.name', '')
- return info
+ if self._build_info is None or self._is_rebooting:
+ info = {}
+ build_info = self.adb.getprops(CACHED_SYSTEM_PROPS)
+ info['build_id'] = build_info['ro.build.id']
+ info['build_type'] = build_info['ro.build.type']
+ info['build_version_codename'] = build_info.get(
+ 'ro.build.version.codename', '')
+ info['build_version_sdk'] = build_info.get('ro.build.version.sdk',
+ '')
+ info['build_product'] = build_info.get('ro.build.product', '')
+ info['debuggable'] = build_info.get('ro.debuggable', '')
+ info['product_name'] = build_info.get('ro.product.name', '')
+ self._build_info = info
+ return info
+ return self._build_info
@property
def is_bootloader(self):
diff --git a/tests/lib/mock_android_device.py b/tests/lib/mock_android_device.py
index 57527b4..959623a 100755
--- a/tests/lib/mock_android_device.py
+++ b/tests/lib/mock_android_device.py
@@ -87,8 +87,9 @@
self.serial = serial
self.fail_br = fail_br
self.fail_br_before_N = fail_br_before_N
+ self.getprops_call_count = 0
if mock_properties is None:
- self.mock_properties = DEFAULT_MOCK_PROPERTIES
+ self.mock_properties = DEFAULT_MOCK_PROPERTIES.copy()
else:
self.mock_properties = mock_properties
if installed_packages is None:
@@ -129,6 +130,7 @@
return self.mock_properties[params]
def getprops(self, params):
+ self.getprops_call_count = self.getprops_call_count + 1
return self.mock_properties
def bugreport(self, args, shell=False, timeout=None):
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index ae81ae1..51c1977 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -277,6 +277,8 @@
self.assertEqual(build_info['build_product'], 'FakeModel')
self.assertEqual(build_info['product_name'], 'FakeModel')
self.assertEqual(build_info['debuggable'], '1')
+ self.assertEqual(
+ len(build_info), len(android_device.CACHED_SYSTEM_PROPS))
@mock.patch(
'mobly.controllers.android_device_lib.adb.AdbProxy',
@@ -303,6 +305,22 @@
@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_AndroidDevice_build_info_cached(self, MockFastboot, MockAdbProxy):
+ """Verifies the AndroidDevice object's basic attributes are correctly
+ set after instantiation.
+ """
+ ad = android_device.AndroidDevice(serial='1')
+ _ = ad.build_info
+ _ = ad.build_info
+ _ = ad.build_info
+ self.assertEqual(ad.adb.getprops_call_count, 1)
+
+ @mock.patch(
+ 'mobly.controllers.android_device_lib.adb.AdbProxy',
return_value=mock_android_device.MockAdbProxy(
'1',
mock_properties={
@@ -965,6 +983,51 @@
'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_reboot_changes_build_info(
+ self, stop_proc_mock, start_proc_mock, FastbootProxy,
+ MockAdbProxy):
+ ad = android_device.AndroidDevice(serial='1')
+ with ad.handle_reboot():
+ ad.adb.mock_properties['ro.build.type'] = 'user'
+ ad.adb.mock_properties['ro.debuggable'] = '0'
+ self.assertEqual(ad.build_info['build_type'], 'user')
+ self.assertEqual(ad.build_info['debuggable'], '0')
+ self.assertFalse(ad.is_rootable)
+ self.assertEqual(ad.adb.getprops_call_count, 2)
+
+ @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_reboot_changes_build_info_with_caching(
+ self, stop_proc_mock, start_proc_mock, FastbootProxy,
+ MockAdbProxy):
+ ad = android_device.AndroidDevice(serial='1') # Call getprops 1.
+ rootable_states = [ad.is_rootable]
+ with ad.handle_reboot():
+ rootable_states.append(ad.is_rootable) # Call getprops 2.
+ ad.adb.mock_properties['ro.debuggable'] = '0'
+ rootable_states.append(ad.is_rootable) # Call getprops 3.
+ # Call getprops 4, on context manager end.
+ rootable_states.append(ad.is_rootable) # Cached call.
+ rootable_states.append(ad.is_rootable) # Cached call.
+ self.assertEqual(ad.adb.getprops_call_count, 4)
+ self.assertEqual(rootable_states, [True, True, False, False, False])
+
+ @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.AndroidDevice.is_boot_completed',
side_effect=[
False, False,