Repository: google/mobly

Issue:
[Snippet Client] Misleading error message in _is_app_installed()
If a app is installed but not instrumented properly, SnippetClient._is_app_installed() will throw an error saying the app is not installed.
The error message should correctly reflect the situation.


Discussion:

Resolving patch:
diff --git a/mobly/controllers/android_device_lib/jsonrpc_client_base.py b/mobly/controllers/android_device_lib/jsonrpc_client_base.py
--- a/mobly/controllers/android_device_lib/jsonrpc_client_base.py
+++ b/mobly/controllers/android_device_lib/jsonrpc_client_base.py
@@ -44,6 +44,8 @@
 import threading
 import time
 
+from mobly.controllers.android_device_lib import adb
+
 # Maximum time to wait for the app to start on the device.
 APP_START_WAIT_TIME = 15
 
@@ -138,23 +140,15 @@ def stop_app(self):
         """
         raise NotImplementedError()
 
-    def _is_app_installed(self):
+    def check_app_installed(self):
         """Checks if app is installed.
 
         Must be implemented by subclasses.
-
-        Returns:
-            True if installed, False otherwise.
         """
         raise NotImplementedError()
 
     # Rest of the client methods.
 
-    def check_app_installed(self):
-      if not self._is_app_installed():
-            raise AppStartError(
-                '%s is not installed on %s' % (self.app_name, self._adb.serial))
-
     def start_app(self, wait_time=APP_START_WAIT_TIME):
         """Starts the server app on the android device.
 
@@ -217,6 +211,25 @@ def close(self):
             self._conn.close()
             self._conn = None
 
+    def _adb_grep_wrapper(self, adb_shell_cmd):
+        """A wrapper for the specific usage of adb shell grep in this class.
+
+        This surpresses AdbError if the grep fails to find anything.
+
+        Args:
+            adb_shell_cmd: A string that is an adb shell cmd with grep.
+
+        Returns:
+            The stdout of the grep result if the grep found something, False
+            otherwise.
+        """
+        try:
+            return self._adb.shell(adb_shell_cmd).decode('utf-8')
+        except adb.AdbError as e:
+            if (e.ret_code == 1) and (not e.stdout) and (not e.stderr):
+                return False
+            raise
+
     def _cmd(self, command, uid=None):
         """Send a command to the server.
 
diff --git a/mobly/controllers/android_device_lib/sl4a_client.py b/mobly/controllers/android_device_lib/sl4a_client.py
--- a/mobly/controllers/android_device_lib/sl4a_client.py
+++ b/mobly/controllers/android_device_lib/sl4a_client.py
@@ -51,13 +51,10 @@ def stop_app(self):
         """Overrides superclass."""
         self._adb.shell('am force-stop com.googlecode.android_scripting')
 
-    def _is_app_installed(self):
+    def check_app_installed(self):
         """Overrides superclass."""
-        try:
-            out = self._adb.shell("pm path com.googlecode.android_scripting"
-                                  ).decode('utf-8').strip()
-            return bool(out)
-        except adb.AdbError as e:
-            if (e.ret_code == 1) and (not e.stdout) and (not e.stderr):
-                return False
-            raise
+        if not self._adb_grep_wrapper(
+            "pm list package | grep com.googlecode.android_scripting"):
+            raise jsonrpc_client_base.AppStartError(
+                '%s is not installed on %s' % (
+                self.app_name, self._adb.getprop('ro.boot.serialno')))
diff --git a/mobly/controllers/android_device_lib/snippet_client.py b/mobly/controllers/android_device_lib/snippet_client.py
--- a/mobly/controllers/android_device_lib/snippet_client.py
+++ b/mobly/controllers/android_device_lib/snippet_client.py
@@ -15,15 +15,16 @@
 # limitations under the License.
 """JSON RPC interface to Mobly Snippet Lib."""
 import logging
+import re
 
 from mobly.controllers.android_device_lib import adb
 from mobly.controllers.android_device_lib import jsonrpc_client_base
 
-_LAUNCH_CMD = ('am instrument -e action start -e port {} '
-               '{}/com.google.android.mobly.snippet.SnippetRunner')
+_INSTRUMENTATION_RUNNER_PACKAGE = 'com.google.android.mobly.snippet.SnippetRunner'
 
-_STOP_CMD = ('am instrument -w -e action stop '
-             '{}/com.google.android.mobly.snippet.SnippetRunner')
+_LAUNCH_CMD = 'am instrument -e action start -e port %s %s/' + _INSTRUMENTATION_RUNNER_PACKAGE
+
+_STOP_CMD = 'am instrument -w -e action stop %s/' + _INSTRUMENTATION_RUNNER_PACKAGE
 
 
 class Error(Exception):
@@ -53,10 +54,11 @@ def __init__(self, package, host_port, adb_proxy):
             host_port=host_port, device_port=host_port, app_name=package,
             adb_proxy=adb_proxy)
         self.package = package
+        self._serial = self._adb.getprop('ro.boot.serialno')
 
     def _do_start_app(self):
         """Overrides superclass."""
-        cmd = _LAUNCH_CMD.format(self.device_port, self.package)
+        cmd = _LAUNCH_CMD % (self.device_port, self.package)
         # Use info here so people know exactly what's happening here, which is
         # helpful since they need to create their own instrumentations and
         # manifest.
@@ -65,21 +67,36 @@ def _do_start_app(self):
 
     def stop_app(self):
         """Overrides superclass."""
-        cmd = _STOP_CMD.format(self.package)
+        cmd = _STOP_CMD % self.package
         logging.info('Stopping snippet apk with: %s', cmd)
-        out = self._adb.shell(_STOP_CMD.format(self.package)).decode('utf-8')
+        out = self._adb.shell(_STOP_CMD % self.package).decode('utf-8')
         if 'OK (0 tests)' not in out:
             raise Error('Failed to stop existing apk. Unexpected output: %s' %
                         out)
 
-    def _is_app_installed(self):
+    def check_app_installed(self):
         """Overrides superclass."""
-        try:
-            out = self._adb.shell(
-                'pm list instrumentation | grep ^instrumentation:%s/' %
-                self.package).decode('utf-8')
-            return bool(out)
-        except adb.AdbError as e:
-            if (e.ret_code == 1) and (not e.stdout) and (not e.stderr):
-                return False
-            raise
+        # Check that the Mobly Snippet app is installed.
+        if not self._adb_grep_wrapper(
+            'pm list package | grep ^package:%s$' % self.package):
+            raise jsonrpc_client_base.AppStartError(
+                '%s is not installed on %s' %
+                (self.package, self._serial))
+        # Check that the app is instrumented.
+        out = self._adb_grep_wrapper(
+            'pm list instrumentation | grep ^instrumentation:%s/%s' %
+            (self.package, _INSTRUMENTATION_RUNNER_PACKAGE))
+        if not out:
+            raise jsonrpc_client_base.AppStartError(
+                '%s is installed on %s, but it is not instrumented.' %
+                    (self.package, self._serial))
+        match = re.search(r'^instrumentation:(.*)\/(.*) \(target=(.*)\)$', out)
+        target_name = match.group(3)
+        # Check that the instrumentation target is installed if it's not the
+        # same as the snippet package.
+        if target_name != self.package:
+            if not self._adb_grep_wrapper(
+                'pm list package | grep ^package:%s$' % target_name):
+                raise jsonrpc_client_base.AppStartError(
+                    'Instrumentation target %s is not installed on %s' %
+                    (target_name, self._serial))


Test patch: