Add infrastructure for Interactive tests

Inspired by the SemiAuto tests used by the test team, this uses a
similar approach but wrapped in an XML-RPC server deployed to the
client.

BUG=chromium:256771
TEST=server test that uses interactive_client

Change-Id: I8420a2a2ed6fc031b97044d6bbcf32e50cbcf4a4
Reviewed-on: https://gerrit.chromium.org/gerrit/63640
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Scott James Remnant <keybuk@chromium.org>
Tested-by: Scott James Remnant <keybuk@chromium.org>
diff --git a/server/cros/bluetooth/bluetooth_client.py b/server/cros/bluetooth/bluetooth_client.py
index 4e6ae4b..38d8a9b 100644
--- a/server/cros/bluetooth/bluetooth_client.py
+++ b/server/cros/bluetooth/bluetooth_client.py
@@ -2,30 +2,28 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import logging
 
-from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros import constants
 from autotest_lib.server import autotest
 
 
 class BluetoothClient(object):
-    """BluetoothClient is a thin layer of logic over a remote DUT."""
+    """BluetoothClient is a thin layer of logic over a remote DUT.
+
+    The Autotest host object representing the remote DUT, passed to this
+    class on initialization, can be accessed from its host property.
+
+    """
 
     XMLRPC_BRINGUP_TIMEOUT_SECONDS = 60
 
-    @property
-    def host(self):
-        """@return host object representing the remote DUT."""
-        return self._host
-
     def __init__(self, client_host):
         """Construct a BluetoothClient.
 
-        @param client_host host object representing a remote host.
+        @param client_host: host object representing a remote host.
 
         """
-        self._host = client_host
+        self.host = client_host
         # Make sure the client library is on the device so that the proxy code
         # is there when we try to call it.
         client_at = autotest.Autotest(self.host)
@@ -40,6 +38,7 @@
                   constants.BLUETOOTH_CLIENT_XMLRPC_SERVER_READY_METHOD,
                 timeout_seconds=self.XMLRPC_BRINGUP_TIMEOUT_SECONDS)
 
+
     def reset_on(self):
         """Reset the adapter and settings and power up the adapter.
 
@@ -48,6 +47,7 @@
         """
         return self._proxy.reset_on()
 
+
     def reset_off(self):
         """Reset the adapter and settings, leave the adapter powered off.
 
@@ -56,39 +56,43 @@
         """
         return self._proxy.reset_off()
 
+
     def set_powered(self, powered):
         """Set the adapter power state.
 
-        @param powered adapter power state to set (True or False).
+        @param powered: adapter power state to set (True or False).
 
         @return True on success, False otherwise.
 
         """
         return self._proxy.set_powered(powered)
 
+
     def set_discoverable(self, discoverable):
         """Set the adapter discoverable state.
 
-        @param powered adapter discoverable state to set (True or False).
+        @param discoverable: adapter discoverable state to set (True or False).
 
         @return True on success, False otherwise.
 
         """
         return self._proxy.set_discoverable(discoverable)
 
+
     def set_pairable(self, pairable):
         """Set the adapter pairable state.
 
-        @param powered adapter pairable state to set (True or False).
+        @param pairable: adapter pairable state to set (True or False).
 
         @return True on success, False otherwise.
 
         """
         return self._proxy.set_pairable(pairable)
 
+
     def close(self):
         """Tear down state associated with the client."""
         # Leave the adapter powered off, but don't do a full reset.
         self._proxy.set_powered(False)
         # This kills the RPC server.
-        self._host.close()
+        self.host.close()
diff --git a/server/cros/bluetooth/bluetooth_tester.py b/server/cros/bluetooth/bluetooth_tester.py
index fde3683..3ad9d0c 100644
--- a/server/cros/bluetooth/bluetooth_tester.py
+++ b/server/cros/bluetooth/bluetooth_tester.py
@@ -2,30 +2,28 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import logging
-
-from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros import constants
 from autotest_lib.server import autotest
 
 
 class BluetoothTester(object):
-    """BluetoothTester is a thin layer of logic over a remote tester."""
+    """BluetoothTester is a thin layer of logic over a remote tester.
+
+    The Autotest host object representing the remote tester, passed to this
+    class on initialization, can be accessed from its host property.
+
+    """
+
 
     XMLRPC_BRINGUP_TIMEOUT_SECONDS = 60
 
-    @property
-    def host(self):
-        """@return host object representing the remote tester."""
-        return self._host
-
     def __init__(self, tester_host):
         """Construct a BluetoothTester.
 
-        @param tester_host host object representing a remote host.
+        @param tester_host: host object representing a remote host.
 
         """
-        self._host = tester_host
+        self.host = tester_host
         # Make sure the client library is on the device so that the proxy code
         # is there when we try to call it.
         client_at = autotest.Autotest(self.host)
@@ -40,7 +38,8 @@
                   constants.BLUETOOTH_TESTER_XMLRPC_SERVER_READY_METHOD,
                 timeout_seconds=self.XMLRPC_BRINGUP_TIMEOUT_SECONDS)
 
+
     def close(self):
         """Tear down state associated with the client."""
         # This kills the RPC server.
-        self._host.close()
+        self.host.close()
diff --git a/server/cros/interactive_client.py b/server/cros/interactive_client.py
new file mode 100644
index 0000000..91e4944
--- /dev/null
+++ b/server/cros/interactive_client.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2013 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.
+
+from autotest_lib.client.cros import constants
+from autotest_lib.server import autotest
+
+
+class InteractiveClient(object):
+    """InteractiveClient represents a remote host for interactive tests.
+
+    An XML-RPC server is deployed to the remote host and a set of methods
+    exposed that allow you to open a browser window on that device, write
+    output and receive button clicks in order to develop interactive tests.
+    """
+
+    XMLRPC_BRINGUP_TIMEOUT_SECONDS = 60
+
+    def __init__(self, client_host):
+        """Construct a InteractiveClient.
+
+        @param client_host: host object representing a remote host.
+
+        """
+        self._host = client_host
+        # Make sure the client library is on the device so that the proxy code
+        # is there when we try to call it.
+        client_at = autotest.Autotest(self._host)
+        client_at.install()
+        # Start up the XML-RPC proxy on the client.
+        self._proxy = self.host.xmlrpc_connect(
+                constants.INTERACTIVE_XMLRPC_SERVER_COMMAND,
+                constants.INTERACTIVE_XMLRPC_SERVER_PORT,
+                command_name=
+                  constants.INTERACTIVE_XMLRPC_SERVER_CLEANUP_PATTERN,
+                ready_test_name=
+                  constants.INTERACTIVE_XMLRPC_SERVER_READY_METHOD,
+                timeout_seconds=self.XMLRPC_BRINGUP_TIMEOUT_SECONDS)
+
+
+    def login(self):
+        """Login to the system and open a tab.
+
+        The tab opened is used by other methods on this server to interact
+        with the user.
+
+        @return True on success, False otherwise.
+
+        """
+        return self._proxy.login()
+
+
+    def set_output(self, html):
+        """Replace the contents of the tab.
+
+        @param html: HTML document to replace tab contents with.
+
+        @return True on success, False otherwise.
+
+        """
+        return self._proxy.set_output(html)
+
+
+    def append_output(self, html):
+        """Append HTML to the contents of the tab.
+
+        @param html: HTML to append to the existing tab contents.
+
+        @return True on success, False otherwise.
+
+        """
+        return self._proxy.append_output(html)
+
+
+    def append_buttons(self, *args):
+        """Append confirmation buttons to the tab.
+
+        Each button is given an index, 0 for the first button, 1 for the second,
+        and so on.
+
+        @param title...: Title of button to append.
+
+        @return True on success, False otherwise.
+
+        """
+        return self._proxy.append_buttons(*args)
+
+
+    def wait_for_button(self, timeout):
+        """Wait for a button to be clicked.
+
+        Call append_buttons() before this to add buttons to the document.
+
+        @param timeout: Maximum time, in seconds, to wait for a click.
+
+        @return index of button that was clicked, or -1 on timeout.
+
+        """
+        return self._proxy.wait_for_button(timeout)
+
+
+    def close(self):
+        """Tear down state associated with the client."""
+        # Log out the browser.
+        self._proxy.close()
+        # This does not close the host because it's shared with the client.