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/client/common_lib/cros/bluetooth/bluetooth_client_xmlrpc_server.py b/client/common_lib/cros/bluetooth/bluetooth_client_xmlrpc_server.py
index af03270..a9b687a 100755
--- a/client/common_lib/cros/bluetooth/bluetooth_client_xmlrpc_server.py
+++ b/client/common_lib/cros/bluetooth/bluetooth_client_xmlrpc_server.py
@@ -11,17 +11,17 @@
import shutil
import common
+from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib.cros import xmlrpc_server
from autotest_lib.client.cros import constants
-from telemetry.core import util
class BluetoothClientXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate):
- """Exposes DUT methods called removely during Bluetooth autotests.
+ """Exposes DUT methods called remotely during Bluetooth autotests.
All instance methods of this object without a preceding '_' are exposed via
an XML-RPC server. This is not a stateless handler object, which means that
- if you store state inside the delegate, that state will remain aroun dfor
+ if you store state inside the delegate, that state will remain around for
future calls.
"""
@@ -71,26 +71,39 @@
self._update_bluez()
self._update_adapter()
+
def _update_bluez(self):
- """Store a D-Bus proxy for the Bluetooth daemon in self._bluez."""
+ """Store a D-Bus proxy for the Bluetooth daemon in self._bluez.
+
+ @return True on success, False otherwise.
+
+ """
self._bluez = None
try:
self._bluez = self._system_bus.get_object(
self.BLUEZ_SERVICE_NAME,
self.BLUEZ_MANAGER_PATH)
logging.debug('bluetoothd is running')
+ return True
except dbus.exceptions.DBusException, e:
if e.get_dbus_name() == self.DBUS_ERROR_SERVICEUNKNOWN:
logging.debug('bluetoothd is not running')
self._bluez = None
+ return False
else:
raise
+
def _update_adapter(self):
- """Store a D-Bus proxy for the local adapter in self._adapter."""
+ """Store a D-Bus proxy for the local adapter in self._adapter.
+
+ @return True on success, including if there is no local adapter,
+ False otherwise.
+
+ """
self._adapter = None
if self._bluez is None:
- return
+ return False
objects = self._bluez.GetManagedObjects(
dbus_interface=self.BLUEZ_MANAGER_IFACE)
@@ -101,7 +114,10 @@
self._adapter = self._system_bus.get_object(
self.BLUEZ_SERVICE_NAME,
path)
- break
+ return True
+ else:
+ return False
+
@xmlrpc_server.dbus_safe(False)
def reset_on(self):
@@ -114,6 +130,7 @@
self._set_powered(True)
return True
+
@xmlrpc_server.dbus_safe(False)
def reset_off(self):
"""Reset the adapter and settings, leave the adapter powered off.
@@ -124,6 +141,7 @@
self._reset()
return True
+
def _reset(self):
"""Reset the Bluetooth adapter and settings."""
logging.debug('_reset')
@@ -143,23 +161,26 @@
self._bluetoothd.Start(dbus.Array(signature='s'), True,
dbus_interface=self.UPSTART_JOB_IFACE)
- # We can't just pass self._update_bluez/adapter to util.WaitFor because
- # it wont take instance functions, so wrap.
+ # We can't just pass self._update_bluez/adapter to poll_for_condition
+ # because we need to check the local state.
logging.debug('waiting for bluez start')
- def start_bluez():
- self._update_bluez()
- return self._bluez is not None
- def get_adapter():
- self._update_adapter()
- return self._adapter is not None
- util.WaitFor(start_bluez, self.ADAPTER_TIMEOUT)
- util.WaitFor(get_adapter, self.ADAPTER_TIMEOUT)
+ utils.poll_for_condition(
+ condition=self._update_bluez,
+ desc='Bluetooth Daemon has started.',
+ timeout=self.ADAPTER_TIMEOUT)
+
+ logging.debug('waiting for bluez to obtain adapter information')
+ utils.poll_for_condition(
+ condition=self._update_adapter,
+ desc='Bluetooth Daemon has adapter information.',
+ timeout=self.ADAPTER_TIMEOUT)
+
@xmlrpc_server.dbus_safe(False)
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.
@@ -167,21 +188,23 @@
self._set_powered(powered)
return True
+
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).
"""
logging.debug('_set_powered %r', powered)
self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Powered', powered,
dbus_interface=dbus.PROPERTIES_IFACE)
+
@xmlrpc_server.dbus_safe(False)
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.
@@ -191,11 +214,12 @@
dbus_interface=dbus.PROPERTIES_IFACE)
return True
+
@xmlrpc_server.dbus_safe(False)
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.
@@ -207,7 +231,7 @@
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
- handler = logging.handlers.SysLogHandler(address = '/dev/log')
+ handler = logging.handlers.SysLogHandler(address='/dev/log')
logging.getLogger().addHandler(handler)
logging.debug('bluetooth_client_xmlrpc_server main...')
server = xmlrpc_server.XmlRpcServer(
diff --git a/client/common_lib/cros/interactive_xmlrpc_server.py b/client/common_lib/cros/interactive_xmlrpc_server.py
new file mode 100755
index 0000000..ec696f6
--- /dev/null
+++ b/client/common_lib/cros/interactive_xmlrpc_server.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+
+# 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.
+
+import cgi
+import json
+import logging
+import logging.handlers
+import os
+import sys
+
+import common
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib.cros import chrome, xmlrpc_server
+from autotest_lib.client.cros import constants
+
+
+class InteractiveXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate):
+ """Exposes methods called remotely to create interactive tests.
+
+ All instance methods of this object without a preceding '_' are exposed via
+ an XML-RPC server. This is not a stateless handler object, which means that
+ if you store state inside the delegate, that state will remain around for
+ future calls.
+ """
+
+ 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.
+
+ """
+ self._chrome = chrome.Chrome()
+ self._chrome.browser.SetHTTPServerDirectories(
+ os.path.dirname(sys.argv[0]))
+ self._tab = self._chrome.browser.tabs[0]
+ self._tab.Navigate(self._chrome.browser.http_server.UrlOf('shell.html'))
+
+ return True
+
+
+ def set_output(self, html):
+ """Replace the contents of the tab.
+
+ @param html: HTML document to replace tab contents with.
+
+ @return True.
+
+ """
+ # JSON does a better job of escaping HTML for JavaScript than we could
+ # with string.replace().
+ html_escaped = json.dumps(html)
+ # Use JavaScript to append the output and scroll to the bottom of the
+ # open tab.
+ self._tab.ExecuteJavaScript('document.body.innerHTML = %s; ' %
+ html_escaped)
+ self._tab.Activate()
+ self._tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ return True
+
+
+ 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.
+
+ """
+ # JSON does a better job of escaping HTML for JavaScript than we could
+ # with string.replace().
+ html_escaped = json.dumps(html)
+ # Use JavaScript to append the output and scroll to the bottom of the
+ # open tab.
+ self._tab.ExecuteJavaScript(
+ ('document.body.innerHTML += %s; ' % html_escaped) +
+ 'window.scrollTo(0, document.body.scrollHeight);')
+ self._tab.Activate()
+ self._tab.WaitForDocumentReadyStateToBeInteractiveOrBetter()
+ return True
+
+
+ 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.
+
+ """
+ html = ''
+ index = 0
+ for title in args:
+ onclick = 'submit_button(%d)' % index
+ html += ('<input type="button" value="%s" onclick="%s">' % (
+ cgi.escape(title),
+ cgi.escape(onclick)))
+ index += 1
+ return self.append_output(html)
+
+
+ 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.
+
+ """
+ # Wait for the button to be clicked.
+ utils.poll_for_condition(
+ condition=lambda:
+ self._tab.EvaluateJavaScript('window.__ready') == 1,
+ desc='User clicked on button.',
+ timeout=timeout)
+ # Fetch the result.
+ result = self._tab.EvaluateJavaScript('window.__result')
+ # Reset for the next button.
+ self._tab.ExecuteJavaScript(
+ 'window.__ready = 0; '
+ 'window.__result = null;')
+ return result
+
+
+ def close(self):
+ """Close the browser.
+
+ @return True.
+
+ """
+ self._chrome.browser.Close()
+ return True
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG)
+ handler = logging.handlers.SysLogHandler(address='/dev/log')
+ logging.getLogger().addHandler(handler)
+ logging.debug('interactive_xmlrpc_server main...')
+ server = xmlrpc_server.XmlRpcServer(
+ 'localhost',
+ constants.INTERACTIVE_XMLRPC_SERVER_PORT)
+ server.register_delegate(InteractiveXmlRpcDelegate())
+ server.run()
diff --git a/client/common_lib/cros/shell.html b/client/common_lib/cros/shell.html
new file mode 100644
index 0000000..5e314cc
--- /dev/null
+++ b/client/common_lib/cros/shell.html
@@ -0,0 +1,25 @@
+<!--
+# 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.
+#
+# This html file is used by Interactive Tests. Scripts are able to append
+# HTML to the document, including buttons that can call submit_button() on
+# click and retrieve the results.
+-->
+<html>
+ <head>
+ <title>Interactive Test</title>
+ <script language="Javascript">
+ window.__ready = 0;
+ window.__result = null;
+ function submit_button(value) {
+ window.__result = value;
+ window.__ready = 1;
+ };
+ </script>
+ </head>
+ <body>
+
+ </body>
+</html>