Repository: google/mobly

Issue:
snippet launching is broken in py3
When trying to launch snippet, it fails with:

```
  File "/Users/angli/Developer/mobly/tools/snippet_shell.py", line 43, in _start_services
    self._ad.load_snippet(name='snippet', package=self._package)
  File "/Users/angli/Developer/mobly/mobly/controllers/android_device.py", line 727, in load_snippet
    client.start_app_and_connect()
  File "/Users/angli/Developer/mobly/mobly/controllers/android_device_lib/snippet_client.py", line 92, in start_app_and_connect
    persists_shell_cmd = self._get_persist_command()
  File "/Users/angli/Developer/mobly/mobly/controllers/android_device_lib/snippet_client.py", line 312, in _get_persist_command
    if command in self._adb.shell('which %s' % command):
TypeError: a bytes-like object is required, not 'str'
```

Looks like this is related to recent change #251 


Discussion:

Resolving patch:
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
@@ -27,8 +27,8 @@
 _LAUNCH_CMD_V0 = ('%s am instrument -w -e action start -e port %s %s/' +
                   _INSTRUMENTATION_RUNNER_PACKAGE)
 
-_LAUNCH_CMD_V1 = (
-    '%s am instrument -w -e action start %s/' + _INSTRUMENTATION_RUNNER_PACKAGE)
+_LAUNCH_CMD_V1 = ('%s am instrument -w -e action start %s/' +
+                  _INSTRUMENTATION_RUNNER_PACKAGE)
 
 _STOP_CMD = (
     'am instrument -w -e action stop %s/' + _INSTRUMENTATION_RUNNER_PACKAGE)
@@ -119,7 +119,8 @@ def start_app_and_connect(self):
             # Reuse the host port as the device port in v0 snippet. This isn't
             # safe in general, but the protocol is deprecated.
             self.device_port = self.host_port
-            cmd = _LAUNCH_CMD_V0 % (persists_shell_cmd, self.device_port, self.package)
+            cmd = _LAUNCH_CMD_V0 % (persists_shell_cmd, self.device_port,
+                                    self.package)
             self._proc = self._do_start_app(cmd)
             self._connect_to_v0()
             self._launch_version = 'v0'
@@ -165,8 +166,8 @@ def restore_app_connection(self, port=None):
             # Failed to connect to app, something went wrong.
             raise jsonrpc_client_base.AppRestoreConnectionError(
                 ('Failed to restore app connection for %s at host port %s, '
-                 'device port %s'),
-                self.package, self.host_port, self.device_port)
+                 'device port %s'), self.package, self.host_port,
+                self.device_port)
 
         # Because the previous connection was lost, update self._proc
         self._proc = None
@@ -250,8 +251,7 @@ def _do_start_app(self, launch_cmd):
     # removed.
     def _connect_to_v0(self):
         self._adb.forward(
-            ['tcp:%d' % self.host_port,
-             'tcp:%d' % self.device_port])
+            ['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port])
         start_time = time.time()
         expiration_time = start_time + _APP_START_WAIT_TIME_V0
         while time.time() < expiration_time:
@@ -270,8 +270,7 @@ def _connect_to_v0(self):
 
     def _connect_to_v1(self):
         self._adb.forward(
-            ['tcp:%d' % self.host_port,
-             'tcp:%d' % self.device_port])
+            ['tcp:%d' % self.host_port, 'tcp:%d' % self.device_port])
         self.connect()
 
     def _read_protocol_line(self):
@@ -309,12 +308,14 @@ def _get_persist_command(self):
         """Check availability and return path of command if available."""
         for command in [_SETSID_COMMAND, _NOHUP_COMMAND]:
             try:
-                if command in self._adb.shell('which %s' % command):
+                if command in self._adb.shell(['which',
+                                               command]).decode('utf-8'):
                     return command
             except adb.AdbError:
                 continue
-        self.log.warning('No %s and %s commands available to launch instrument '
-                         'persistently, tests that depend on UiAutomator and '
-                         'at the same time performs USB disconnection may fail',
-                         _SETSID_COMMAND, _NOHUP_COMMAND)
+        self.log.warning(
+            'No %s and %s commands available to launch instrument '
+            'persistently, tests that depend on UiAutomator and '
+            'at the same time performs USB disconnection may fail',
+            _SETSID_COMMAND, _NOHUP_COMMAND)
         return ''


Test patch:
diff --git a/tests/mobly/controllers/android_device_lib/snippet_client_test.py b/tests/mobly/controllers/android_device_lib/snippet_client_test.py
--- a/tests/mobly/controllers/android_device_lib/snippet_client_test.py
+++ b/tests/mobly/controllers/android_device_lib/snippet_client_test.py
@@ -53,7 +53,7 @@ def shell(self, params, shell=False):
                 p=MOCK_PACKAGE_NAME,
                 r=snippet_client._INSTRUMENTATION_RUNNER_PACKAGE), 'utf-8')
         elif 'which' in params:
-            return ''
+            return b''
 
     def __getattr__(self, name):
         """All calls to the none-existent functions in adb proxy would
@@ -196,7 +196,7 @@ def _mocked_shell(arg):
             if 'setsid' in arg:
                 raise adb.AdbError('cmd', 'stdout', 'stderr', 'ret_code')
             else:
-                return 'nohup'
+                return b'nohup'
 
         mock_get_port.return_value = 123
         mock_read_protocol_line.side_effect = [
@@ -210,7 +210,7 @@ def _mocked_shell(arg):
 
         # Test 'setsid' exists
         client = self._make_client()
-        client._adb.shell = mock.Mock(return_value='setsid')
+        client._adb.shell = mock.Mock(return_value=b'setsid')
         client.start_app_and_connect()
         cmd_setsid = '%s am instrument -w -e action start %s/%s' % (
             snippet_client._SETSID_COMMAND,