Chameleon: Enable Chameleon client-side support

This change introduces a ChameleonConnection abstraction which unifies the
Chameleon connection from both client-side and server-side. A client test
sets this connection up by given either a lab-DUT hostname or specified in
the command line arguments.

The example test case display_ClientChameleonConnection is added. It verifies
the basic hardware setup of Chameleon from a client-side test.

BUG=chromium:405143
TEST=Ran the old server test display_Resolution.mirrored passed.
TEST=Ran the new client test display_ClientChameleonConnection passed, using:
$ test_that --board peppy --args "chameleon_host=$CHAMELEON_IP" $DUT_IP \
  display_ClientChameleonConnection
TEST=Disconnected the HDMI cable, ran the same test failed.

Change-Id: Iac4c247ddbf9f8dac462f9c1ecb8cda4df74f4a3
Reviewed-on: https://chromium-review.googlesource.com/213709
Tested-by: Wai-Hong Tam <waihong@chromium.org>
Reviewed-by: Hung-ying Tyan <tyanh@chromium.org>
Commit-Queue: Wai-Hong Tam <waihong@chromium.org>
diff --git a/client/cros/chameleon/chameleon.py b/client/cros/chameleon/chameleon.py
index f621411..91a5026 100644
--- a/client/cros/chameleon/chameleon.py
+++ b/client/cros/chameleon/chameleon.py
@@ -2,27 +2,85 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import httplib
+import socket
 import time
 import xmlrpclib
 
 from PIL import Image
 
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros.chameleon import edid
 
 
+CHAMELEON_PORT = 9992
+
+
+class ChameleonConnectionError(error.TestError):
+    """Indicates that connecting to Chameleon failed.
+
+    It is fatal to the test unless caught.
+    """
+    pass
+
+
+class ChameleonConnection(object):
+    """ChameleonConnection abstracts the network connection to the board.
+
+    ChameleonBoard and ChameleonPort use it for accessing Chameleon RPC.
+
+    """
+
+    def __init__(self, hostname, port=CHAMELEON_PORT):
+        """Constructs a ChameleonConnection.
+
+        @param hostname: Hostname the chameleond process is running.
+        @param port: Port number the chameleond process is listening on.
+
+        @raise ChameleonConnectionError if connection failed.
+        """
+        self.chameleond_proxy = ChameleonConnection._create_server_proxy(
+                hostname, port)
+
+
+    @staticmethod
+    def _create_server_proxy(hostname, port):
+        """Creates the chameleond server proxy.
+
+        @param hostname: Hostname the chameleond process is running.
+        @param port: Port number the chameleond process is listening on.
+
+        @return ServerProxy object to chameleond.
+
+        @raise ChameleonConnectionError if connection failed.
+        """
+        remote = 'http://%s:%s' % (hostname, port)
+        chameleond_proxy = xmlrpclib.ServerProxy(remote, allow_none=True)
+        # Call a RPC to test.
+        try:
+            chameleond_proxy.ProbeInputs()
+        except (socket.error,
+                xmlrpclib.ProtocolError,
+                httplib.BadStatusLine) as e:
+            raise ChameleonConnectionError(e)
+        return chameleond_proxy
+
+
 class ChameleonBoard(object):
     """ChameleonBoard is an abstraction of a Chameleon board.
 
     A Chameleond RPC proxy is passed to the construction such that it can
     use this proxy to control the Chameleon board.
+
     """
 
-    def __init__(self, chameleond_proxy):
+    def __init__(self, chameleon_connection):
         """Construct a ChameleonBoard.
 
-        @param chameleond_proxy: Chameleond RPC proxy object.
+        @param chameleon_connection: ChameleonConnection object.
         """
-        self._chameleond_proxy = chameleond_proxy
+        self._chameleond_proxy = chameleon_connection.chameleond_proxy
 
 
     def reset(self):
@@ -201,3 +259,45 @@
         # The return value of RPC is converted to a list. Convert it back to
         # a tuple.
         return tuple(self._chameleond_proxy.DetectResolution(self._input_id))
+
+
+def make_chameleon_hostname(dut_hostname):
+    """Given a DUT's hostname, returns the hostname of its Chameleon.
+
+    @param dut_hostname: Hostname of a DUT.
+
+    @return Hostname of the DUT's Chameleon.
+    """
+    host_parts = dut_hostname.split('.')
+    host_parts[0] = host_parts[0] + '-chameleon'
+    return '.'.join(host_parts)
+
+
+def create_chameleon_board(dut_hostname, args):
+    """Given either DUT's hostname or argments, creates a ChameleonBoard object.
+
+    If the DUT's hostname is in the lab zone, it connects to the Chameleon by
+    append the hostname with '-chameleon' suffix. If not, checks if the args
+    contains the key-value pair 'chameleon_host=IP'.
+
+    @param dut_hostname: Hostname of a DUT.
+    @param args: A string of arguments passed from the command line.
+
+    @return A ChameleonBoard object.
+
+    @raise ChameleonConnectionError if unknown hostname.
+    """
+    connection = None
+    hostname = make_chameleon_hostname(dut_hostname)
+    if utils.host_is_in_lab_zone(hostname):
+        connection = ChameleonConnection(hostname)
+    else:
+        args_dict = utils.args_to_dict(args)
+        hostname = args_dict.get('chameleon_host', None)
+        port = args_dict.get('chameleon_port', CHAMELEON_PORT)
+        if hostname:
+            connection = ChameleonConnection(hostname, port)
+        else:
+            raise ChameleonConnectionError('No chameleon_host is given in args')
+
+    return ChameleonBoard(connection)
diff --git a/client/site_tests/display_ClientChameleonConnection/control b/client/site_tests/display_ClientChameleonConnection/control
new file mode 100644
index 0000000..1df14b2
--- /dev/null
+++ b/client/site_tests/display_ClientChameleonConnection/control
@@ -0,0 +1,21 @@
+# Copyright 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = 'chromeos-chameleon'
+NAME = 'display_ClientChameleonConnection'
+PURPOSE = 'Chameleon connection test from client-side.'
+CRITERIA = 'This test fails if DUT and Chameleon are not connected properly.'
+SUITE = 'chameleon_dp, chameleon_hdmi'
+TIME = 'SHORT'
+TEST_CATEGORY = 'Functional'
+TEST_CLASS = 'display'
+TEST_TYPE = 'client'
+DEPENDENCIES = 'chameleon'
+
+DOC = """
+This test checks the connection between DUT and Chameleon.
+"""
+
+host = next(iter(job.hosts))
+job.run_test('display_ClientChameleonConnection', host=host, args=args)
diff --git a/client/site_tests/display_ClientChameleonConnection/display_ClientChameleonConnection.py b/client/site_tests/display_ClientChameleonConnection/display_ClientChameleonConnection.py
new file mode 100755
index 0000000..02b6b66
--- /dev/null
+++ b/client/site_tests/display_ClientChameleonConnection/display_ClientChameleonConnection.py
@@ -0,0 +1,65 @@
+# Copyright 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This is a client-side test to check the Chameleon connection."""
+
+import logging
+
+from autotest_lib.client.bin import test
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.cros.chameleon import chameleon
+
+
+class display_ClientChameleonConnection(test.test):
+    """Chameleon connection client test.
+
+    This test talks to a Chameleon board from DUT. Try to plug the Chameleon
+    ports and see if DUT detects them.
+    """
+    version = 1
+
+    _TIMEOUT_VIDEO_STABLE_PROBE = 10
+
+
+    def run_once(self, host, args):
+        self.chameleon = chameleon.create_chameleon_board(host.hostname, args)
+        self.chameleon.reset()
+
+        connected_ports = []
+        dut_failed_ports = []
+        for chameleon_port in self.chameleon.get_all_ports():
+            connector_type = chameleon_port.get_connector_type()
+            # Try to plug the port such that DUT can detect it.
+            chameleon_port.plug()
+            # DUT takes some time to respond. Wait until the video signal
+            # to stabilize.
+            chameleon_port.wait_video_input_stable(
+                    self._TIMEOUT_VIDEO_STABLE_PROBE)
+
+            # Add the connected ports if they are detected by xrandr.
+            xrandr_output = utils.get_xrandr_output_state()
+            for output in xrandr_output.iterkeys():
+                if output.startswith(connector_type):
+                    connected_ports.append(chameleon_port)
+                    break
+            else:
+                dut_failed_ports.append(chameleon_port)
+
+            # Unplug the port afterward.
+            chameleon_port.unplug()
+
+        if connected_ports:
+            ports_to_str = lambda ports: ', '.join(
+                    '%s(%d)' % (p.get_connector_type(), p.get_connector_id())
+                    for p in ports)
+            logging.info('Detected %d connected ports: %s',
+                         len(connected_ports), ports_to_str(connected_ports))
+            if dut_failed_ports:
+                message = 'DUT failed to detect Chameleon ports: %s' % (
+                        ports_to_str(dut_failed_ports))
+                logging.error(message)
+                raise error.TestFail(message)
+        else:
+            raise error.TestFail('No port connected to Chameleon')
diff --git a/server/hosts/chameleon_host.py b/server/hosts/chameleon_host.py
index 99a5687..3d96209 100644
--- a/server/hosts/chameleon_host.py
+++ b/server/hosts/chameleon_host.py
@@ -6,26 +6,11 @@
 """This file provides core logic for connecting a Chameleon Daemon."""
 
 
-import xmlrpclib
-
 from autotest_lib.client.bin import utils
 from autotest_lib.client.cros.chameleon import chameleon
 from autotest_lib.server.hosts import ssh_host
 
 
-def make_chameleon_hostname(dut_hostname):
-    """Given a DUT's hostname, return the hostname of its Chameleon.
-
-    @param dut_hostname: hostname of a DUT.
-
-    @return hostname of the DUT's Chameleon.
-
-    """
-    host_parts = dut_hostname.split('.')
-    host_parts[0] = host_parts[0] + '-chameleon'
-    return '.'.join(host_parts)
-
-
 class ChameleonHost(ssh_host.SSHHost):
     """Host class for a host that controls a Chameleon."""
 
@@ -51,8 +36,8 @@
         super(ChameleonHost, self)._initialize(hostname=chameleon_host,
                                                *args, **dargs)
         self._is_in_lab = utils.host_is_in_lab_zone(self.hostname)
-        remote = 'http://%s:%s' % (self.hostname, chameleon_port)
-        self._chameleond_proxy = xmlrpclib.ServerProxy(remote, allow_none=True)
+        self._chameleon_connection = chameleon.ChameleonConnection(
+                self.hostname, chameleon_port)
 
 
     def is_in_lab(self):
@@ -81,7 +66,7 @@
         """Create a ChameleonBoard object."""
         # TODO(waihong): Add verify and repair logic which are required while
         # deploying to Cros Lab.
-        return chameleon.ChameleonBoard(self._chameleond_proxy)
+        return chameleon.ChameleonBoard(self._chameleon_connection)
 
 
 def create_chameleon_host(dut, chameleon_args):
@@ -105,7 +90,7 @@
     @returns: A ChameleonHost object or None.
 
     """
-    hostname = make_chameleon_hostname(dut)
+    hostname = chameleon.make_chameleon_hostname(dut)
     if utils.host_is_in_lab_zone(hostname):
         return ChameleonHost(chameleon_host=hostname)
     elif chameleon_args: