Repository: google/mobly

Issue:
`snippet_client._start_event_client` crashes
because of the extra `host_port` arg.

This crashes all tests that use `@AsyncRpc`.

We should add some unit tests for this...


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
@@ -138,10 +138,8 @@ def stop_app(self):
     def _start_event_client(self):
         """Overrides superclass."""
         event_client = SnippetClient(
-            package=self.package,
-            host_port=self.host_port,
-            adb_proxy=self._adb,
-            log=self.log)
+            package=self.package, adb_proxy=self._adb, log=self.log)
+        event_client.host_port = self.host_port
         event_client.connect(self.uid,
                              jsonrpc_client_base.JsonRpcCommand.CONTINUE)
         return event_client


Test patch:
diff --git a/tests/lib/jsonrpc_client_test_base.py b/tests/lib/jsonrpc_client_test_base.py
new file mode 100755
--- /dev/null
+++ b/tests/lib/jsonrpc_client_test_base.py
@@ -0,0 +1,71 @@
+# Copyright 2017 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.
+
+from builtins import str
+
+import mock
+import unittest
+
+
+class JsonRpcClientTestBase(unittest.TestCase):
+    """Base class for tests of JSONRPC clients.
+
+    Contains infrastructure for mocking responses.
+    """
+
+    MOCK_RESP = (
+        b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1, '
+        b'"callback": null}')
+    MOCK_RESP_WITHOUT_CALLBACK = (
+        b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1}')
+    MOCK_RESP_TEMPLATE = (
+        '{"id": %d, "result": 123, "error": null, "status": 1, "uid": 1, '
+        '"callback": null}')
+    MOCK_RESP_UNKNOWN_STATUS = (
+        b'{"id": 0, "result": 123, "error": null, "status": 0, '
+        b'"callback": null}')
+    MOCK_RESP_WITH_CALLBACK = (
+        b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1, '
+        b'"callback": "1-0"}')
+    MOCK_RESP_WITH_ERROR = b'{"id": 0, "error": 1, "status": 1, "uid": 1}'
+
+    class MockSocketFile(object):
+        def __init__(self, resp):
+            self.resp = resp
+            self.last_write = None
+
+        def write(self, msg):
+            self.last_write = msg
+
+        def readline(self):
+            return self.resp
+
+        def flush(self):
+            pass
+
+    def setup_mock_socket_file(self, mock_create_connection, resp=MOCK_RESP):
+        """Sets up a fake socket file from the mock connection.
+
+        Args:
+            mock_create_connection: The mock method for creating a method.
+            resp: (str) response to give. MOCK_RESP by default.
+
+        Returns:
+            The mock file that will be injected into the code.
+        """
+        fake_file = self.MockSocketFile(resp)
+        fake_conn = mock.MagicMock()
+        fake_conn.makefile.return_value = fake_file
+        mock_create_connection.return_value = fake_conn
+        return fake_file
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -127,8 +127,8 @@ def test_never(self):
 
         bt_cls = MockBaseTest(self.mock_test_cls_configs)
         expected_msg = ('Test method name not_a_test_something does not follow '
-                        'naming convention test_*, abort.')
-        with self.assertRaises(base_test.Error, msg=expected_msg):
+                        'naming convention test_\*, abort.')
+        with self.assertRaisesRegexp(base_test.Error, expected_msg):
             bt_cls.run(test_names=["not_a_test_something"])
 
     def test_default_execution_of_all_tests(self):
@@ -790,9 +790,9 @@ def test_unpack_userparams_required_missing(self):
         """Missing a required param should raise an error."""
         required = ["something"]
         bc = base_test.BaseTestClass(self.mock_test_cls_configs)
-        expected_msg = ("Missing required user param '%s' in test "
-                        "configuration.") % required[0]
-        with self.assertRaises(base_test.Error, msg=expected_msg):
+        expected_msg = ('Missing required user param "%s" in test '
+                        'configuration.') % required[0]
+        with self.assertRaisesRegexp(base_test.Error, expected_msg):
             bc.unpack_userparams(required)
 
     def test_unpack_userparams_optional(self):
diff --git a/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py b/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
--- a/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
+++ b/tests/mobly/controllers/android_device_lib/jsonrpc_client_base_test.py
@@ -20,33 +20,7 @@
 import unittest
 
 from mobly.controllers.android_device_lib import jsonrpc_client_base
-
-MOCK_RESP = (b'{"id": 0, "result": 123, "error": null, "status": 1, "uid": 1,'
-             b' "callback": null}')
-MOCK_RESP_WITHOUT_CALLBACK = (b'{"id": 0, "result": 123, "error": null, '
-                              b'"status": 1, "uid": 1}')
-MOCK_RESP_TEMPLATE = ('{"id": %d, "result": 123, "error": null, "status": 1, '
-                      '"uid": 1, "callback": null}')
-MOCK_RESP_UNKNOWN_STATUS = (b'{"id": 0, "result": 123, "error": null, '
-                            b'"status": 0, "callback": null}')
-MOCK_RESP_WITH_CALLBACK = (b'{"id": 0, "result": 123, "error": null, '
-                           b'"status": 1, "uid": 1, "callback": "1-0"}')
-MOCK_RESP_WITH_ERROR = b'{"id": 0, "error": 1, "status": 1, "uid": 1}'
-
-
-class MockSocketFile(object):
-    def __init__(self, resp):
-        self.resp = resp
-        self.last_write = None
-
-    def write(self, msg):
-        self.last_write = msg
-
-    def readline(self):
-        return self.resp
-
-    def flush(self):
-        pass
+from tests.lib import jsonrpc_client_test_base
 
 
 class FakeRpcClient(jsonrpc_client_base.JsonRpcClientBase):
@@ -54,25 +28,10 @@ def __init__(self):
         super(FakeRpcClient, self).__init__(app_name='FakeRpcClient')
 
 
-class JsonRpcClientBaseTest(unittest.TestCase):
+class JsonRpcClientBaseTest(jsonrpc_client_test_base.JsonRpcClientTestBase):
     """Unit tests for mobly.controllers.android_device_lib.jsonrpc_client_base.
     """
 
-    def setup_mock_socket_file(self, mock_create_connection):
-        """Sets up a fake socket file from the mock connection.
-
-        Args:
-            mock_create_connection: The mock method for creating a method.
-
-        Returns:
-            The mock file that will be injected into the code.
-        """
-        fake_file = MockSocketFile(MOCK_RESP)
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = fake_file
-        mock_create_connection.return_value = fake_conn
-        return fake_file
-
     @mock.patch('socket.create_connection')
     def test_open_timeout_io_error(self, mock_create_connection):
         """Test socket timeout with io error
@@ -104,11 +63,11 @@ def test_handshake_error(self, mock_create_connection):
         Test that if there is an error in the jsonrpc handshake then a protocol
         error will be raised.
         """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(None)
-        mock_create_connection.return_value = fake_conn
-        with self.assertRaises(jsonrpc_client_base.ProtocolError):
-            client = FakeRpcClient()
+        self.setup_mock_socket_file(mock_create_connection, resp=None)
+        client = FakeRpcClient()
+        with self.assertRaisesRegexp(
+                jsonrpc_client_base.ProtocolError,
+                jsonrpc_client_base.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
             client.connect()
 
     @mock.patch('socket.create_connection')
@@ -118,13 +77,9 @@ def test_connect_handshake(self, mock_create_connection):
         Test that at the end of a handshake with no errors the client object
         has the correct parameters.
         """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(MOCK_RESP)
-        mock_create_connection.return_value = fake_conn
-
+        self.setup_mock_socket_file(mock_create_connection)
         client = FakeRpcClient()
         client.connect()
-
         self.assertEqual(client.uid, 1)
 
     @mock.patch('socket.create_connection')
@@ -134,36 +89,12 @@ def test_connect_handshake_unknown_status(self, mock_create_connection):
         Test that when the handshake is given an unknown status then the client
         will not be given a uid.
         """
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = MockSocketFile(
-            MOCK_RESP_UNKNOWN_STATUS)
-        mock_create_connection.return_value = fake_conn
-
+        self.setup_mock_socket_file(
+            mock_create_connection, resp=self.MOCK_RESP_UNKNOWN_STATUS)
         client = FakeRpcClient()
         client.connect()
-
         self.assertEqual(client.uid, jsonrpc_client_base.UNKNOWN_UID)
 
-    @mock.patch('socket.create_connection')
-    def test_connect_no_response(self, mock_create_connection):
-        """Test handshake no response
-
-        Test that if a handshake recieves no response then it will give a
-        protocol error.
-        """
-        fake_file = self.setup_mock_socket_file(mock_create_connection)
-
-        client = FakeRpcClient()
-        client.connect()
-
-        fake_file.resp = None
-
-        with self.assertRaises(
-                jsonrpc_client_base.ProtocolError,
-                msg=
-                jsonrpc_client_base.ProtocolError.NO_RESPONSE_FROM_HANDSHAKE):
-            client.some_rpc(1, 2, 3)
-
     @mock.patch('socket.create_connection')
     def test_rpc_error_response(self, mock_create_connection):
         """Test rpc that is given an error response
@@ -176,9 +107,9 @@ def test_rpc_error_response(self, mock_create_connection):
         client = FakeRpcClient()
         client.connect()
 
-        fake_file.resp = MOCK_RESP_WITH_ERROR
+        fake_file.resp = self.MOCK_RESP_WITH_ERROR
 
-        with self.assertRaises(jsonrpc_client_base.ApiError, msg=1):
+        with self.assertRaisesRegexp(jsonrpc_client_base.ApiError, '1'):
             client.some_rpc(1, 2, 3)
 
     @mock.patch('socket.create_connection')
@@ -193,7 +124,7 @@ def test_rpc_callback_response(self, mock_create_connection):
         client = FakeRpcClient()
         client.connect()
 
-        fake_file.resp = MOCK_RESP_WITH_CALLBACK
+        fake_file.resp = self.MOCK_RESP_WITH_CALLBACK
         client._event_client = mock.Mock()
 
         callback = client.some_rpc(1, 2, 3)
@@ -212,11 +143,11 @@ def test_rpc_id_mismatch(self, mock_create_connection):
         client = FakeRpcClient()
         client.connect()
 
-        fake_file.resp = (MOCK_RESP_TEMPLATE % 52).encode('utf8')
+        fake_file.resp = (self.MOCK_RESP_TEMPLATE % 52).encode('utf8')
 
-        with self.assertRaises(
+        with self.assertRaisesRegexp(
                 jsonrpc_client_base.ProtocolError,
-                msg=jsonrpc_client_base.ProtocolError.MISMATCHED_API_ID):
+                jsonrpc_client_base.ProtocolError.MISMATCHED_API_ID):
             client.some_rpc(1, 2, 3)
 
     @mock.patch('socket.create_connection')
@@ -233,9 +164,9 @@ def test_rpc_no_response(self, mock_create_connection):
 
         fake_file.resp = None
 
-        with self.assertRaises(
+        with self.assertRaisesRegexp(
                 jsonrpc_client_base.ProtocolError,
-                msg=jsonrpc_client_base.ProtocolError.NO_RESPONSE_FROM_SERVER):
+                jsonrpc_client_base.ProtocolError.NO_RESPONSE_FROM_SERVER):
             client.some_rpc(1, 2, 3)
 
     @mock.patch('socket.create_connection')
@@ -265,10 +196,8 @@ def test_rpc_send_to_socket_without_callback(self, mock_create_connection):
 
         Logic is the same as test_rpc_send_to_socket.
         """
-        fake_file = MockSocketFile(MOCK_RESP_WITHOUT_CALLBACK)
-        fake_conn = mock.MagicMock()
-        fake_conn.makefile.return_value = fake_file
-        mock_create_connection.return_value = fake_conn
+        fake_file = self.setup_mock_socket_file(
+            mock_create_connection, resp=self.MOCK_RESP_WITHOUT_CALLBACK)
 
         client = FakeRpcClient()
         client.connect()
@@ -293,11 +222,11 @@ def test_rpc_call_increment_counter(self, mock_create_connection):
         client.connect()
 
         for i in range(0, 10):
-            fake_file.resp = (MOCK_RESP_TEMPLATE % i).encode('utf-8')
+            fake_file.resp = (self.MOCK_RESP_TEMPLATE % i).encode('utf-8')
             client.some_rpc()
 
         self.assertEquals(next(client._counter), 10)
 
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     unittest.main()
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
@@ -20,10 +20,11 @@
 
 from mobly.controllers.android_device_lib import jsonrpc_client_base
 from mobly.controllers.android_device_lib import snippet_client
+from tests.lib import jsonrpc_client_test_base
 
 MOCK_PACKAGE_NAME = 'some.package.name'
 MOCK_MISSING_PACKAGE_NAME = 'not.installed'
-JSONRPC_BASE_PACKAGE = 'mobly.controllers.android_device_lib.jsonrpc_client_base.JsonRpcClientBase'
+JSONRPC_BASE_CLASS = 'mobly.controllers.android_device_lib.jsonrpc_client_base.JsonRpcClientBase'
 
 
 class MockAdbProxy(object):
@@ -63,47 +64,62 @@ def adb_call(*args):
         return adb_call
 
 
-class JsonRpcClientBaseTest(unittest.TestCase):
+class SnippetClientTest(jsonrpc_client_test_base.JsonRpcClientTestBase):
     """Unit tests for mobly.controllers.android_device_lib.snippet_client.
     """
 
-    @mock.patch('socket.create_connection')
-    @mock.patch(JSONRPC_BASE_PACKAGE)
-    def test_check_app_installed_normal(self, mock_create_connection,
-                                        mock_client_base):
+    def test_check_app_installed_normal(self):
         sc = self._make_client()
         sc._check_app_installed()
 
-    @mock.patch('socket.create_connection')
-    @mock.patch(JSONRPC_BASE_PACKAGE)
-    def test_check_app_installed_fail_app_not_installed(
-            self, mock_create_connection, mock_client_base):
+    def test_check_app_installed_fail_app_not_installed(self):
         sc = self._make_client(MockAdbProxy(apk_not_installed=True))
         expected_msg = '%s is not installed on .*' % MOCK_PACKAGE_NAME
         with self.assertRaisesRegexp(jsonrpc_client_base.AppStartError,
                                      expected_msg):
             sc._check_app_installed()
 
-    @mock.patch('socket.create_connection')
-    @mock.patch(JSONRPC_BASE_PACKAGE)
-    def test_check_app_installed_fail_not_instrumented(
-            self, mock_create_connection, mock_client_base):
+    def test_check_app_installed_fail_not_instrumented(self):
         sc = self._make_client(MockAdbProxy(apk_not_instrumented=True))
         expected_msg = '%s is installed on .*, but it is not instrumented.' % MOCK_PACKAGE_NAME
         with self.assertRaisesRegexp(jsonrpc_client_base.AppStartError,
                                      expected_msg):
             sc._check_app_installed()
 
-    @mock.patch('socket.create_connection')
-    @mock.patch(JSONRPC_BASE_PACKAGE)
-    def test_check_app_installed_fail_target_not_installed(
-            self, mock_create_connection, mock_client_base):
+    def test_check_app_installed_fail_target_not_installed(self):
         sc = self._make_client(MockAdbProxy(target_not_installed=True))
         expected_msg = 'Instrumentation target %s is not installed on .*' % MOCK_MISSING_PACKAGE_NAME
         with self.assertRaisesRegexp(jsonrpc_client_base.AppStartError,
                                      expected_msg):
             sc._check_app_installed()
 
+    @mock.patch('socket.create_connection')
+    def test_snippet_start(self, mock_create_connection):
+        self.setup_mock_socket_file(mock_create_connection)
+        client = self._make_client()
+        client.connect()
+        result = client.testSnippetCall()
+        self.assertEqual(123, result)
+
+    @mock.patch('socket.create_connection')
+    def test_snippet_start_event_client(self, mock_create_connection):
+        fake_file = self.setup_mock_socket_file(mock_create_connection)
+        client = self._make_client()
+        client.host_port = 123  # normally picked by start_app_and_connect
+        client.connect()
+        fake_file.resp = self.MOCK_RESP_WITH_CALLBACK
+        callback = client.testSnippetCall()
+        self.assertEqual(123, callback.ret_value)
+        self.assertEqual('1-0', callback._id)
+
+        # Check to make sure the event client is using the same port as the
+        # main client.
+        self.assertEqual(123, callback._event_client.host_port)
+
+        fake_file.resp = self.MOCK_RESP_WITH_ERROR
+        with self.assertRaisesRegexp(jsonrpc_client_base.ApiError, '1'):
+            callback.getAll('eventName')
+
     def _make_client(self, adb_proxy=MockAdbProxy()):
         return snippet_client.SnippetClient(
             package=MOCK_PACKAGE_NAME, adb_proxy=adb_proxy)