Repository: google/mobly

Issue:
Add is_emulator property to AndroidDevice

Discussion:

Patch:
diff --git a/mobly/controllers/android_device.py b/mobly/controllers/android_device.py
index cb9ab52..c3197ca 100644
--- a/mobly/controllers/android_device.py
+++ b/mobly/controllers/android_device.py
@@ -18,6 +18,7 @@ from past.builtins import basestring
 import contextlib
 import logging
 import os
+import re
 import shutil
 import time
 
@@ -52,8 +53,10 @@ CACHED_SYSTEM_PROPS = [
     'ro.build.version.codename',
     'ro.build.version.sdk',
     'ro.build.product',
+    'ro.build.characteristics',
     'ro.debuggable',
     'ro.product.name',
+    'ro.hardware',
 ]
 
 # Keys for attributes in configs that alternate the controller module behavior.
@@ -78,6 +81,9 @@ Error = errors.Error
 DeviceError = errors.DeviceError
 SnippetError = snippet_management_service.Error
 
+# Regex to heuristically determine if the device is an emulator.
+EMULATOR_SERIAL_REGEX = re.compile(r'emulator-\d+')
+
 
 def create(configs):
     """Creates AndroidDevice controller objects.
@@ -338,7 +344,6 @@ def get_devices(ads, **kwargs):
     Raises:
         Error: No devices are matched.
     """
-
     def _get_device_filter(ad):
         for k, v in kwargs.items():
             if not hasattr(ad, k):
@@ -443,7 +448,6 @@ class AndroidDevice(object):
         services: ServiceManager, the manager of long-running services on the
             device.
     """
-
     def __init__(self, serial=''):
         self._serial = str(serial)
         # logging.log_path only exists when this is used in an Mobly test run.
@@ -763,8 +767,11 @@ class AndroidDevice(object):
             info['build_version_sdk'] = build_info.get('ro.build.version.sdk',
                                                        '')
             info['build_product'] = build_info.get('ro.build.product', '')
+            info['build_characteristics'] = build_info.get(
+                'ro.build.characteristics', '')
             info['debuggable'] = build_info.get('ro.debuggable', '')
             info['product_name'] = build_info.get('ro.product.name', '')
+            info['hardware'] = build_info.get('ro.hardware', '')
             self._build_info = info
             return info
         return self._build_info
@@ -810,6 +817,31 @@ class AndroidDevice(object):
             return model
         return self.build_info['product_name'].lower()
 
+    @property
+    def is_emulator(self):
+        """Whether this device is probably an emulator.
+
+        Returns:
+            True if this is probably an emulator.
+        """
+        if EMULATOR_SERIAL_REGEX.match(self.serial):
+            # If the device's serial follows 'emulator-dddd', then it's almost
+            # certainly an emulator.
+            return True
+        elif self.build_info['build_characteristics'] == 'emulator':
+            # If the device says that it's an emulator, then it's probably an
+            # emulator although some real devices apparently report themselves
+            # as emulators in addition to other things, so only return True on
+            # an exact match.
+            return True
+        elif self.build_info['hardware'] in ['ranchu', 'goldfish']:
+            # Ranchu and Goldfish are the hardware properties that the AOSP
+            # emulators report, so if the device says it's an AOSP emulator, it
+            # probably is one.
+            return True
+        else:
+            return False
+
     def load_config(self, config):
         """Add attributes to the AndroidDevice object based on config.
 
@@ -1049,7 +1081,6 @@ class AndroidDeviceLoggerAdapter(logging.LoggerAdapter):
     Then each log line added by my_log will have a prefix
     '[AndroidDevice|<tag>]'
     """
-
     def process(self, msg, kwargs):
         msg = _DEBUG_PREFIX_TEMPLATE % (self.extra['tag'], msg)
         return (msg, kwargs)


Test patch:
diff --git a/tests/lib/mock_android_device.py b/tests/lib/mock_android_device.py
index 959623a..28c90e6 100755
--- a/tests/lib/mock_android_device.py
+++ b/tests/lib/mock_android_device.py
@@ -30,6 +30,8 @@ DEFAULT_MOCK_PROPERTIES = {
     'ro.product.name': 'FakeModel',
     'ro.debuggable': '1',
     'sys.boot_completed': "1",
+    'ro.build.characteristics': 'emulator,phone',
+    'ro.hardware': 'marlin',
 }
 
 
@@ -76,7 +78,6 @@ def list_adb_devices():
 
 class MockAdbProxy(object):
     """Mock class that swaps out calls to adb with mock calls."""
-
     def __init__(self,
                  serial='',
                  fail_br=False,
@@ -115,13 +116,16 @@ class MockAdbProxy(object):
             packages = self.installed_packages + [
                 package for package, _, _ in self.instrumented_packages
             ]
-            return bytes('\n'.join(
-                ['package:%s' % package for package in packages]), 'utf-8')
+            return bytes(
+                '\n'.join(['package:%s' % package for package in packages]),
+                'utf-8')
         elif 'pm list instrumentation' in params:
-            return bytes('\n'.join([
-                'instrumentation:%s/%s (target=%s)' % (package, runner, target)
-                for package, runner, target in self.instrumented_packages
-            ]), 'utf-8')
+            return bytes(
+                '\n'.join([
+                    'instrumentation:%s/%s (target=%s)' %
+                    (package, runner, target)
+                    for package, runner, target in self.instrumented_packages
+                ]), 'utf-8')
         elif 'which' in params:
             return b''
 
@@ -144,7 +148,6 @@ class MockAdbProxy(object):
         """All calls to the none-existent functions in adb proxy would
         simply return the adb command string.
         """
-
         def adb_call(*args, **kwargs):
             arg_str = ' '.join(str(elem) for elem in args)
             return arg_str
@@ -154,7 +157,6 @@ class MockAdbProxy(object):
 
 class MockFastbootProxy(object):
     """Mock class that swaps out calls to adb with mock calls."""
-
     def __init__(self, serial):
         self.serial = serial
 
diff --git a/tests/mobly/controllers/android_device_test.py b/tests/mobly/controllers/android_device_test.py
index 4438490..f3f8e0c 100755
--- a/tests/mobly/controllers/android_device_test.py
+++ b/tests/mobly/controllers/android_device_test.py
@@ -44,7 +44,6 @@ class AndroidDeviceTest(unittest.TestCase):
     """This test class has unit tests for the implementation of everything
     under mobly.controllers.android_device.
     """
-
     def setUp(self):
         # Set log_path to logging since mobly logger setup is not called.
         if not hasattr(logging, 'log_path'):
@@ -280,8 +279,10 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertEqual(build_info['build_version_codename'], 'Z')
         self.assertEqual(build_info['build_version_sdk'], '28')
         self.assertEqual(build_info['build_product'], 'FakeModel')
+        self.assertEqual(build_info['build_characteristics'], 'emulator,phone')
         self.assertEqual(build_info['product_name'], 'FakeModel')
         self.assertEqual(build_info['debuggable'], '1')
+        self.assertEqual(build_info['hardware'], 'marlin')
         self.assertEqual(len(build_info),
                          len(android_device.CACHED_SYSTEM_PROPS))
 
@@ -303,8 +304,10 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertEqual(build_info['build_version_codename'], '')
         self.assertEqual(build_info['build_version_sdk'], '')
         self.assertEqual(build_info['build_product'], '')
+        self.assertEqual(build_info['build_characteristics'], '')
         self.assertEqual(build_info['product_name'], '')
         self.assertEqual(build_info['debuggable'], '')
+        self.assertEqual(build_info['hardware'], '')
 
     @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
                 return_value=mock_android_device.MockAdbProxy('1'))
@@ -384,6 +387,95 @@ class AndroidDeviceTest(unittest.TestCase):
         self.assertTrue(isinstance(ad.serial, str))
         yaml.safe_dump(ad.serial)
 
+    @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_is_emulator_when_realish_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='1')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy('localhost:123'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('localhost:123'))
+    def test_AndroidDevice_is_emulator_when_local_networked_device(
+            self, MockFastboot, MockAdbProxy):
+        # Although these devices are usually emulators, there might be a reason
+        # to do this with a real device.
+        ad = android_device.AndroidDevice(serial='localhost:123')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch(
+        'mobly.controllers.android_device_lib.adb.AdbProxy',
+        return_value=mock_android_device.MockAdbProxy('example.com:123'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('example:123'))
+    def test_AndroidDevice_is_emulator_when_remote_networked_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='example.com:123')
+        self.assertFalse(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    'localhost:5554',
+                    mock_properties={
+                        'ro.hardware': 'ranchu',
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                    }))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('localhost:5554'))
+    def test_AndroidDevice_is_emulator_when_ranchu_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='localhost:5554')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    '1',
+                    mock_properties={
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                        'ro.hardware': 'goldfish',
+                    }))
+    @mock.patch('mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+                return_value=mock_android_device.MockFastbootProxy('1'))
+    def test_AndroidDevice_is_emulator_when_goldfish_device(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='1')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy(
+                    'example.com:123',
+                    mock_properties={
+                        'ro.build.id': 'AB42',
+                        'ro.build.type': 'userdebug',
+                        'ro.build.characteristics': 'emulator',
+                    }))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('example.com:123'))
+    def test_AndroidDevice_is_emulator_when_emulator_characteristic(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='example.com:123')
+        self.assertTrue(ad.is_emulator)
+
+    @mock.patch('mobly.controllers.android_device_lib.adb.AdbProxy',
+                return_value=mock_android_device.MockAdbProxy('emulator-5554'))
+    @mock.patch(
+        'mobly.controllers.android_device_lib.fastboot.FastbootProxy',
+        return_value=mock_android_device.MockFastbootProxy('emulator-5554'))
+    def test_AndroidDevice_is_emulator_when_emulator_serial(
+            self, MockFastboot, MockAdbProxy):
+        ad = android_device.AndroidDevice(serial='emulator-5554')
+        self.assertTrue(ad.is_emulator)
+
     @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',