[autotest] Adds chromecast deps, server, client test for sonic.
Adds an autotest dependency that installs the freshest
version of the chromecast extension as an autotest dep.
Also adds a server and client test for sonic that installs the
extension and performs a tab cast, and starts the youtube/netflix apps.
The tests will only work when the DUT and the sonic device are on
the same subnet.
TEST=emerge-lumpy autotest-deps, ran a test that uses it.
Ran the tests, visually verified results.
BUG=chromium:321177, chromium:321179, chromium:292640
Change-Id: I48a6149a11d3bb8167a1c766873e672b3523d769
Reviewed-on: https://chromium-review.googlesource.com/179407
Commit-Queue: Prashanth B <beeps@chromium.org>
Tested-by: Prashanth B <beeps@chromium.org>
Reviewed-by: Dan Shi <dshi@chromium.org>
diff --git a/client/common_lib/cros/chromedriver.py b/client/common_lib/cros/chromedriver.py
index a118433..65bd0a0 100644
--- a/client/common_lib/cros/chromedriver.py
+++ b/client/common_lib/cros/chromedriver.py
@@ -41,8 +41,10 @@
assert os.geteuid() == 0, 'Need superuser privileges'
# Log in with telemetry
- self._browser = chrome.Chrome(extension_paths=extension_paths,
- is_component=is_component).browser
+ self._chrome = chrome.Chrome(extension_paths=extension_paths,
+ is_component=is_component,
+ extra_browser_args=extra_chrome_flags)
+ self._browser = self._chrome.browser
# Start ChromeDriver server
self._server = chromedriver_server(CHROMEDRIVER_EXE_PATH)
@@ -81,6 +83,16 @@
del self._browser
+ def get_extension(self, extension_path):
+ """Gets an extension by proxying to the browser.
+
+ @param extension_path: Path to the extension loaded in the browser.
+
+ @return: A telemetry extension object representing the extension.
+ """
+ return self._chrome.get_extension(extension_path)
+
+
class chromedriver_server(object):
"""A running ChromeDriver server.
diff --git a/client/common_lib/site_utils.py b/client/common_lib/site_utils.py
index 69f769c..1a116c7 100644
--- a/client/common_lib/site_utils.py
+++ b/client/common_lib/site_utils.py
@@ -2,11 +2,13 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import glob
import logging
import os
import re
import signal
import socket
+import sys
import time
import urllib2
@@ -272,3 +274,32 @@
ver = match.group(0) if match else version_string
milestone = match.group(1) if match else ''
return ver, milestone
+
+
+def take_screenshot(dest_dir, fname_prefix, format='png'):
+ """Take screenshot and save to a new file in the dest_dir.
+
+ @param dest_dir: The destination directory to save the screenshot.
+ @param fname_prefix: Prefix for the output fname
+ @param format: String indicating file format ('png', 'jpg', etc)
+
+ @return: The path of the saved screenshot file
+ """
+ next_index = len(glob.glob(
+ os.path.join(dest_dir, '%s-*.%s' % (fname_prefix, format))))
+ screenshot_file = os.path.join(
+ dest_dir, '%s-%d.%s' % (fname_prefix, next_index, format))
+ logging.info('Saving screenshot to %s.', screenshot_file)
+
+ old_exc_type = sys.exc_info()[0]
+ try:
+ base_utils.system('DISPLAY=:0.0 XAUTHORITY=/home/chronos/.Xauthority '
+ '/usr/local/bin/import -window root -depth 8 %s' %
+ screenshot_file)
+ except Exception as err:
+ # Do not raise an exception if the screenshot fails while processing
+ # another exception.
+ if old_exc_type is None:
+ raise
+ logging.error(err)
+ return screenshot_file
diff --git a/client/cros/cros_ui_test.py b/client/cros/cros_ui_test.py
index bf1fd5d..f8d7c8a 100644
--- a/client/cros/cros_ui_test.py
+++ b/client/cros/cros_ui_test.py
@@ -350,37 +350,6 @@
return [cryptohome.canonicalize(name), passwd]
- def take_screenshot(self, fname_prefix, format='png'):
- """Take screenshot and save to a new file in the results dir.
-
- Args:
- fname_prefix: prefix for the output fname
- format: string indicating file format ('png', 'jpg', etc)
-
- Returns:
- the path of the saved screenshot file
- """
- next_index = len(glob.glob(
- os.path.join(self.resultsdir, '%s-*.%s' % (fname_prefix, format))))
- screenshot_file = os.path.join(
- self.resultsdir, '%s-%d.%s' % (fname_prefix, next_index, format))
- logging.info('Saving screenshot to %s.' % screenshot_file)
-
- old_exc_type = sys.exc_info()[0]
- try:
- utils.system('DISPLAY=:0.0 XAUTHORITY=/home/chronos/.Xauthority '
- '/usr/local/bin/import -window root -depth 8 %s' %
- screenshot_file)
- except Exception as err:
- # Do not raise an exception if the screenshot fails while processing
- # another exception.
- if old_exc_type is None:
- raise
- logging.error(err)
-
- return screenshot_file
-
-
def login(self, username=None, password=None):
"""Log in with a set of credentials.
@@ -436,7 +405,7 @@
'file named %s.png in the results folder.' %
(err, screenshot_name))
finally:
- self.take_screenshot(fname_prefix=screenshot_name)
+ utils.take_screenshot(self.resultsdir, fname_prefix=screenshot_name)
self.stop_tcpdump(fname_prefix='tcpdump-lo--till-login')
logging.info('Logged in as %s. You can verify with the '
@@ -544,7 +513,7 @@
constraints=constraints,
*args, **kwargs)
except:
- self.take_screenshot(fname_prefix='test-fail-screenshot')
+ utils.take_screenshot(self.resultsdir, fname_prefix='test-fail-screenshot')
raise
diff --git a/client/deps/sonic_extension/common.py b/client/deps/sonic_extension/common.py
new file mode 100644
index 0000000..3dd0d3b
--- /dev/null
+++ b/client/deps/sonic_extension/common.py
@@ -0,0 +1,15 @@
+# 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 os, sys
+
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+client_dir = os.path.abspath(os.path.join(dirname, os.pardir, os.pardir))
+sys.path.insert(0, client_dir)
+
+import setup_modules
+
+sys.path.pop(0)
+setup_modules.setup(base_path=client_dir,
+ root_module_name="autotest_lib.client")
diff --git a/client/deps/sonic_extension/control b/client/deps/sonic_extension/control
new file mode 100644
index 0000000..8f5d349
--- /dev/null
+++ b/client/deps/sonic_extension/control
@@ -0,0 +1,12 @@
+# 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.
+
+DOC = """\
+Dep required if a test needs to access the eureka extension.
+
+The intallation of this dep involves getting the latest stable
+build of the extension from an omaha server.
+"""
+
+job.setup_dep(['sonic_extension'])
diff --git a/client/deps/sonic_extension/sonic_extension.py b/client/deps/sonic_extension/sonic_extension.py
new file mode 100644
index 0000000..d698d92
--- /dev/null
+++ b/client/deps/sonic_extension/sonic_extension.py
@@ -0,0 +1,120 @@
+#!/usr/bin/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 httplib2
+import json
+import os
+import re
+import shutil
+import urllib2
+
+import common
+
+from autotest_lib.client.common_lib import utils
+
+version = 1
+
+TEST_EXTENSION_ID = 'hfaagokkkhdbgiakmmlclaapfelnkoah'
+UPDATE_CHECK_URL = ('https://clients2.google.com/service/update2/')
+UPDATE_CHECK_PARAMETER = ('crx?x=id%%3D%s%%26v%%3D0%%26uc')
+MANIFEST_KEY = ('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+hlN5FB+tjCsBszmBIvI'
+ 'cD/djLLQm2zZfFygP4U4/o++ZM91EWtgII10LisoS47qT2TIOg4Un4+G57e'
+ 'lZ9PjEIhcJfANqkYrD3t9dpEzMNr936TLB2u683B5qmbB68Nq1Eel7KVc+F'
+ '0BqhBondDqhvDvGPEV0vBsbErJFlNH7SQIDAQAB')
+
+
+def get_download_url_from_omaha(extension_id):
+ """Retrieves an update url from omaha for the specified extension id.
+
+ @param extension_id: The extension id of the chromecast extension.
+
+ @return: A url to download the extension from.
+
+ @raises IOError: If the response returned by the omaha server is invalid.
+ """
+ update_check_link = '%s%s' % (UPDATE_CHECK_URL,
+ UPDATE_CHECK_PARAMETER % extension_id)
+ response_xml = httplib2.Http().request(update_check_link, 'GET')[1]
+ codebase_match = re.compile(r'codebase="(.*crx)"').search(response_xml)
+ if codebase_match is not None:
+ return codebase_match.groups()[0]
+ raise IOError('Omaha response is invalid %s.' % response_xml)
+
+
+def download_extension(dest_file):
+ """Retrieve the extension into a destination crx file.
+
+ @param dest_file: Path to a destination file for the extension.
+ """
+ download_url = get_download_url_from_omaha(TEST_EXTENSION_ID)
+ response = urllib2.urlopen(download_url)
+ with open(dest_file, 'w') as f:
+ f.write(response.read())
+
+
+def fix_public_key(extracted_extension_folder):
+ """Modifies the manifest.json to include a public key.
+
+ This function will erase the content in the original manifest
+ and replace it with a new manifest that contains the key.
+
+ @param extracted_extension_folder: The folder containing
+ the extracted extension.
+ """
+ manifest_json_file = os.path.join(extracted_extension_folder,
+ 'manifest.json')
+ with open(manifest_json_file, 'r') as f:
+ manifest_json = json.loads(f.read())
+
+ manifest_json['key'] = MANIFEST_KEY
+
+ with open(manifest_json_file, 'w') as f:
+ f.write(json.dumps(manifest_json))
+
+
+def setup(output_crx, unzipped_crx_dir):
+ """Setup for tests that need a chromecast extension.
+
+ Download the extension from an omaha server, unzip it and modify its
+ manifest.json to include a public key.
+
+ @param output_crx: The name of the crx file into which we will download
+ the extension. If this file already exists it will get
+ re-written.
+ @param unzipped_crx_dir: A directory for the unzipped extension.
+
+ @raises CmdTimeoutError: If we timeout unzipping the extension.
+ """
+ download_extension(output_crx)
+ unzip_cmd = 'unzip -o "%s" -d "%s"' % (output_crx, unzipped_crx_dir)
+
+ # The unzip command will return a non-zero exit status if there are
+ # extra bytes at the start/end of the zipfile. This is not a critical
+ # failure and the extension will still work.
+ cmd_output = utils.run(unzip_cmd, ignore_status=True, timeout=1)
+ if not os.path.exists(unzipped_crx_dir):
+ raise IOError('Unzip failed, command %s, stderr %s', unzip_cmd,
+ cmd_output.stderr)
+
+ # TODO(beeps): crbug.com/325869, investigate the limits of component
+ # extensions. For now this is ok because even sonic testing inlines a
+ # public key for their test extension.
+ fix_public_key(unzipped_crx_dir)
+
+
+srcdir = os.path.join(os.getcwd(), 'src')
+output_crx = os.path.join(srcdir, 'sonic_extension.crx')
+unzipped_crx_dir = os.path.join(os.path.dirname(output_crx),
+ re.split('[.]', os.path.basename(output_crx))[0])
+
+try:
+ setup(output_crx, unzipped_crx_dir)
+except:
+ if os.path.exists(unzipped_crx_dir):
+ shutil.rmtree(unzipped_crx_dir, ignore_errors=True)
+ raise
+finally:
+ if os.path.exists(output_crx):
+ os.remove(output_crx)
diff --git a/client/site_tests/desktopui_SonicExtension/control b/client/site_tests/desktopui_SonicExtension/control
new file mode 100644
index 0000000..ed209b0
--- /dev/null
+++ b/client/site_tests/desktopui_SonicExtension/control
@@ -0,0 +1,45 @@
+# 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.
+
+AUTHOR = "beeps@chromium.org"
+NAME = "desktopui_SonicExtension"
+PURPOSE = "Verify that we can load the sonic extension and cast a tab."
+CRITERIA = """
+This test will fail if we are unable to load the extension, if we don't
+find the expected test utilities page, or we can't cast a tab to the sonic
+device.
+"""
+TIME = "SHORT"
+TEST_CATEGORY = "General"
+TEST_CLASS = "desktopui"
+TEST_TYPE = "client"
+
+DOC = """
+This test loads an extension through chromedriver and navigates to a test
+utility page used to configure the extension. It requires the ip address
+of a sonic host, and that the sonic host is on the same subnet as the
+Device Under Test.
+
+Usage: test_that <ip address of DUT> --board=<boardname of DUT>
+ --args="sonic_hostname=<ip or name of sonichost>,
+ extension_path=/path/to/local/extension_in_testdir"
+
+The extension_path is optional, if none is specified the default ToT
+extension will get used. If one is specified, it must reside in the
+test's root directory (i.e autotest_checkout/client/site_tests/\
+ desktopui_SonicExtension/).
+"""
+
+args_dict = utils.args_to_dict(args)
+sonic_hostname = args_dict.get('sonic_hostname')
+extension_path = args_dict.get('extension_path')
+if not sonic_hostname:
+ raise error.TestError('Cannot run sonic_AppTest without a sonic host.'
+ 'please specify --args="sonic_hostname=<ip>" with '
+ 'test_that.')
+
+
+job.run_test('desktopui_SonicExtension',
+ extension_path=extension_path,
+ chromecast_ip=sonic_hostname)
diff --git a/client/site_tests/desktopui_SonicExtension/desktopui_SonicExtension.py b/client/site_tests/desktopui_SonicExtension/desktopui_SonicExtension.py
new file mode 100755
index 0000000..df64488
--- /dev/null
+++ b/client/site_tests/desktopui_SonicExtension/desktopui_SonicExtension.py
@@ -0,0 +1,141 @@
+# 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 os
+import json
+import time
+
+import common
+
+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.common_lib.cros import chromedriver
+from autotest_lib.client.cros import httpd
+
+
+class desktopui_SonicExtension(test.test):
+ """Test loading the sonic extension through chromedriver."""
+ version = 1
+ cast_delay = 20
+
+
+ def _install_extension(self):
+ dep = 'sonic_extension'
+ dep_dir = os.path.join(self.autodir, 'deps', dep)
+ self.job.install_pkg(dep, 'dep', dep_dir)
+ return os.path.join(dep_dir, 'src', dep)
+
+
+ def _check_manifest(self, extension_path):
+ """Checks the manifest for a public key.
+
+ The sonic extension is an autotest dependency and will get
+ installed through install_pkg as a component extension (with
+ a public key). Any other method of installation is supported
+ too, as long as it has a public key.
+
+ @param extension_path: A path to the directory of the extension
+ that contains a manifest.json.
+
+ @raises TestError: If the extension doesn't have a public key.
+ """
+ manifest_json_file = os.path.join(extension_path, 'manifest.json')
+ with open(manifest_json_file, 'r') as f:
+ manifest_json = json.loads(f.read())
+ if not manifest_json.get('key'):
+ raise error.TestError('Not a component extension, cannot '
+ 'proceed with sonic test')
+
+
+ def tab_cast(self, driver, chromecast_ip, extension_id):
+ """Tab cast through the extension.
+
+ @param driver: A chromedriver instance that has the chromecast
+ extension loaded.
+ @param chromecast_ip: The ip of the chromecast device to cast to.
+ @param extension_id: Id of the extension to use.
+ """
+ extension_url = 'chrome-extension://%s' % extension_id
+ driver.get('%s/%s' % (extension_url, self._test_utils_page))
+ if driver.title != self._test_utils_title:
+ raise error.TestError('Getting title failed, got title: %s'
+ % driver.title)
+ driver.find_element_by_id('receiverIpAddress').send_keys(
+ chromecast_ip)
+ driver.find_element_by_id('urlToOpen').send_keys(self._test_url)
+ driver.find_element_by_id('mirrorUrl').click()
+
+
+ def initialize(self, extension_path=None, live=False):
+ """Initialize the test.
+
+ @param extension_path: Path to the extension.
+ @param live: Use a live url if True. Start a test server
+ and server a hello world page if False.
+ """
+ super(desktopui_SonicExtension, self).initialize()
+
+ if not extension_path:
+ extension_path = self._install_extension()
+ if not os.path.exists(extension_path):
+ raise error.TestError('Failed to install sonic extension.')
+ self._check_manifest(extension_path)
+ self._extension_path = extension_path
+ self._test_utils_page = 'e2e_test_utils.html'
+ self._test_utils_title = 'Google Cast extension E2E test utilities'
+ self._whitelist_id = 'enhhojjnijigcajfphajepfemndkmdlo'
+
+ if live:
+ self._test_url = 'http://www.google.com'
+ self._test_server = None
+ else:
+ self._test_url = 'http://localhost:8000/hello.html'
+ self._test_server = httpd.HTTPListener(8000, docroot=self.bindir)
+ self._test_server.run()
+
+
+ def cleanup(self):
+ """Clean up the test environment, e.g., stop local http server."""
+ if self._test_server:
+ self._test_server.stop()
+ super(desktopui_SonicExtension, self).cleanup()
+
+
+ def _close_popups(self, driver):
+ """Close any popup windows the extension might open by default.
+
+ @param driver: Chromedriver instance.
+ """
+ for h in driver.window_handles[1:]:
+ driver.switch_to_window(h)
+ driver.close()
+ driver.switch_to_window(driver.window_handles[0])
+
+
+ def run_once(self, chromecast_ip):
+ """Run the test code."""
+
+ # TODO: When we've cloned the sonic test repo get these from their
+ # test config files.
+ kwargs = {
+ 'extension_paths' : [self._extension_path],
+ 'is_component' : True,
+ 'extra_chrome_flags': ['--no-proxy-server', '--start-maximized',
+ '--disable-web-security',
+ '--enable-experimental-extension-apis',
+ '--enable-logging=stderr', '--v=2',
+ ('--whitelisted-extension-id=%s' %
+ self._whitelist_id)],
+ }
+
+ with chromedriver.chromedriver(**kwargs) as chromedriver_instance:
+ driver = chromedriver_instance.driver
+ self._close_popups(driver)
+ extension = chromedriver_instance.get_extension(
+ self._extension_path)
+ self.tab_cast(driver, chromecast_ip, extension.extension_id)
+ time.sleep(self.cast_delay)
+ utils.take_screenshot(self.resultsdir, 'sonic_screenshot_e2e_page')
+
diff --git a/client/site_tests/desktopui_SonicExtension/hello.html b/client/site_tests/desktopui_SonicExtension/hello.html
new file mode 100755
index 0000000..28e1fff
--- /dev/null
+++ b/client/site_tests/desktopui_SonicExtension/hello.html
@@ -0,0 +1,13 @@
+<html>
+
+<head>
+ <META HTTP-EQUIV="Set-Cookie" CONTENT="mykey=myval">
+ <title>Hello World</title>
+</head>
+
+<body>
+ <b><h1>Hello there!</h1></b>
+</body>
+
+</html>
+
diff --git a/server/cros/eureka_client.py b/server/cros/eureka_client.py
deleted file mode 100644
index 1a7f1ba..0000000
--- a/server/cros/eureka_client.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# 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 httplib
-import json
-import logging
-import socket
-import time
-import urllib2
-
-import common
-
-from autotest_lib.client.common_lib import error
-from autotest_lib.client.common_lib.cros import retry
-
-# Give all our rpcs about six seconds of retry time. If a longer timeout
-# is desired one should retry from the caller, this timeout is only meant
-# to avoid uncontrolled circumstances like network flake, not, say, retry
-# right across a reboot.
-BASE_REQUEST_TIMEOUT = 0.1
-JSON_HEADERS = {'Content-Type': 'application/json'}
-RPC_EXCEPTIONS = (httplib.BadStatusLine, socket.error, urllib2.HTTPError)
-
-
-@retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT)
-def _get(url):
- """Get request to the give url.
-
- @raises: Any of the retry exceptions, if we hit the timeout.
- @raises: error.TimeoutException, if the call itself times out.
- eg: a hanging urlopen will get killed with a TimeoutException while
- multiple retries that hit different Http errors will raise the last
- HttpError instead of the TimeoutException.
- """
- return urllib2.urlopen(url).read()
-
-
-@retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT)
-def _post(url, data):
- """Post data to the given url.
-
- @param data: Json data to post.
-
- @raises: Any of the retry exceptions, if we hit the timeout.
- @raises: error.TimeoutException, if the call itself times out.
- For examples see docstring for _get method.
- """
- request = urllib2.Request(url, json.dumps(data),
- headers=JSON_HEADERS)
- urllib2.urlopen(request)
-
-
-class EurekaProxyException(Exception):
- """Generic exception raised when a eureka rpc fails."""
- pass
-
-
-class EurekaProxy(object):
- """Client capable of making calls to the eureka device server."""
- POLLING_INTERVAL = 5
- SETUP_SERVER_PORT = '8008'
- EUREKA_SETUP_SERVER = 'http://%s:%s/setup'
-
- def __init__(self, hostname):
- """
- @param host: The host object representing the Eureka device.
- """
- self._eureka_setup_server = (self.EUREKA_SETUP_SERVER %
- (hostname, self.SETUP_SERVER_PORT))
-
-
- def get_info(self):
- """Returns information about the eureka device.
-
- @return: A dictionary containing information about the eureka device.
- """
- eureka_info_url = '%s/%s' % (self._eureka_setup_server, 'eureka_info')
- try:
- return json.loads(_get(eureka_info_url))
- except (RPC_EXCEPTIONS, error.TimeoutException) as e:
- raise EurekaProxyException('Could not retrieve information about '
- 'eureka device: %s' % e)
-
-
- def get_build_number(self, timeout_mins=0.1):
- """
- Returns the build number of the build on the device.
-
- @param timeout_mins: Timeout in minutes. By default this should
- return almost immediately and hence has a timeout of 6 seconds.
- If we're rebooting, and would like the boot id of the build after
- the reboot is complete this timeout should be in O(minutes).
-
- @raises EurekaProxyException: If unable to get build number within the
- timeout specified.
- """
- current_time = int(time.time())
- end_time = current_time + timeout_mins*60
-
- while end_time > current_time:
- try:
- eureka_info = self.get_info()
- except EurekaProxyException:
- pass
- else:
- return eureka_info.get('build_version', None)
- time.sleep(self.POLLING_INTERVAL)
- current_time = int(time.time())
-
- raise EurekaProxyException('Timed out trying to get build number.')
-
-
- def reboot(self, when="now"):
- """
- Post to the server asking for a reboot.
-
- @param when: The time till reboot. Can be any of:
- now: immediately
- fdr: set factory data reset flag and reboot now
- ota: set recovery flag and reboot now
- ota fdr: set both recovery and fdr flags, and reboot now
- ota foreground: reboot and start force update page
- idle: reboot only when idle screen usage > 10 mins
-
- @raises EurekaProxyException: if we're unable to post a reboot request.
- """
- reboot_url = '%s/%s' % (self._eureka_setup_server, 'reboot')
- reboot_params = {"params": when}
- logging.info('Rebooting device through %s.', reboot_url)
- try:
- _post(reboot_url, reboot_params)
- except (RPC_EXCEPTIONS, error.TimeoutException) as e:
- raise EurekaProxyException('Could not reboot eureka device through '
- '%s: %s' % (self.SETUP_SERVER_PORT, e))
diff --git a/server/cros/sonic_client_utils.py b/server/cros/sonic_client_utils.py
new file mode 100644
index 0000000..72d0403
--- /dev/null
+++ b/server/cros/sonic_client_utils.py
@@ -0,0 +1,217 @@
+# 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 base64
+import hashlib
+import httplib
+import json
+import logging
+import socket
+import StringIO
+import urllib2
+import urlparse
+
+try:
+ import pycurl
+except ImportError:
+ pycurl = None
+
+
+import common
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import retry
+
+
+# Give all our rpcs about six seconds of retry time. If a longer timeout
+# is desired one should retry from the caller, this timeout is only meant
+# to avoid uncontrolled circumstances like network flake, not, say, retry
+# right across a reboot.
+BASE_REQUEST_TIMEOUT = 0.1
+JSON_HEADERS = {'Content-Type': 'application/json'}
+RPC_EXCEPTIONS = (httplib.BadStatusLine, socket.error, urllib2.HTTPError)
+MANIFEST_KEY = ('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+hlN5FB+tjCsBszmBIvI'
+ 'cD/djLLQm2zZfFygP4U4/o++ZM91EWtgII10LisoS47qT2TIOg4Un4+G57e'
+ 'lZ9PjEIhcJfANqkYrD3t9dpEzMNr936TLB2u683B5qmbB68Nq1Eel7KVc+F'
+ '0BqhBondDqhvDvGPEV0vBsbErJFlNH7SQIDAQAB')
+
+
+def get_extension_id(pub_key_pem=MANIFEST_KEY):
+ """Computes the extension id from the public key.
+
+ @param pub_key_pem: The public key used in the extension.
+
+ @return: The extension id.
+ """
+ pub_key_der = base64.b64decode(pub_key_pem)
+ sha = hashlib.sha256(pub_key_der).hexdigest()
+ prefix = sha[:32]
+ reencoded = ""
+ ord_a = ord('a')
+ for old_char in prefix:
+ code = int(old_char, 16)
+ new_char = chr(ord_a + code)
+ reencoded += new_char
+ return reencoded
+
+
+class Url(object):
+ """Container for URL information."""
+
+ def __init__(self):
+ self.scheme = 'http'
+ self.netloc = ''
+ self.path = ''
+ self.params = ''
+ self.query = ''
+ self.fragment = ''
+
+ def Build(self):
+ """Returns the URL."""
+ return urlparse.urlunparse((
+ self.scheme,
+ self.netloc,
+ self.path,
+ self.params,
+ self.query,
+ self.fragment))
+
+
+# TODO(beeps): Move get and post to curl too, since we have the need for
+# custom requests anyway.
+@retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT)
+def _curl_request(host, app_path, port, custom_request='', payload=None):
+ """Sends a custom request throug pycurl, to the url specified.
+ """
+ url = Url()
+ url.netloc = ':'.join((host, str(port)))
+ url.path = app_path
+ full_url = url.Build()
+
+ response = StringIO.StringIO()
+ conn = pycurl.Curl()
+ conn.setopt(conn.URL, full_url)
+ conn.setopt(conn.WRITEFUNCTION, response.write)
+ if custom_request:
+ conn.setopt(conn.CUSTOMREQUEST, custom_request)
+ if payload:
+ conn.setopt(conn.POSTFIELDS, payload)
+ conn.perform()
+ conn.close()
+ return response.getvalue()
+
+
+@retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT)
+def _get(url):
+ """Get request to the give url.
+
+ @raises: Any of the retry exceptions, if we hit the timeout.
+ @raises: error.TimeoutException, if the call itself times out.
+ eg: a hanging urlopen will get killed with a TimeoutException while
+ multiple retries that hit different Http errors will raise the last
+ HttpError instead of the TimeoutException.
+ """
+ return urllib2.urlopen(url).read()
+
+
+@retry.retry(RPC_EXCEPTIONS, timeout_min=BASE_REQUEST_TIMEOUT)
+def _post(url, data):
+ """Post data to the given url.
+
+ @param data: Json data to post.
+
+ @raises: Any of the retry exceptions, if we hit the timeout.
+ @raises: error.TimeoutException, if the call itself times out.
+ For examples see docstring for _get method.
+ """
+ request = urllib2.Request(url, json.dumps(data),
+ headers=JSON_HEADERS)
+ urllib2.urlopen(request)
+
+
+class SonicProxyException(Exception):
+ """Generic exception raised when a sonic rpc fails."""
+ pass
+
+
+class SonicProxy(object):
+ """Client capable of making calls to the sonic device server."""
+ POLLING_INTERVAL = 5
+ SONIC_SERVER_PORT = '8008'
+
+ def __init__(self, hostname):
+ """
+ @param hostname: The name of the host for this sonic proxy.
+ """
+ self._sonic_server = 'http://%s:%s' % (hostname, self.SONIC_SERVER_PORT)
+ self._hostname = hostname
+
+
+ def check_server(self):
+ """Checks if the sonic server is up and running.
+
+ @raises: SonicProxyException if the server is unreachable.
+ """
+ try:
+ json.loads(_get(self._sonic_server))
+ except (RPC_EXCEPTIONS, error.TimeoutException) as e:
+ raise SonicProxyException('Could not retrieve information about '
+ 'sonic device: %s' % e)
+
+
+ def reboot(self, when="now"):
+ """
+ Post to the server asking for a reboot.
+
+ @param when: The time till reboot. Can be any of:
+ now: immediately
+ fdr: set factory data reset flag and reboot now
+ ota: set recovery flag and reboot now
+ ota fdr: set both recovery and fdr flags, and reboot now
+ ota foreground: reboot and start force update page
+ idle: reboot only when idle screen usage > 10 mins
+
+ @raises SonicProxyException: if we're unable to post a reboot request.
+ """
+ reboot_url = '%s/%s/%s' % (self._sonic_server, 'setup', 'reboot')
+ reboot_params = {"params": when}
+ logging.info('Rebooting device through %s.', reboot_url)
+ try:
+ _post(reboot_url, reboot_params)
+ except (RPC_EXCEPTIONS, error.TimeoutException) as e:
+ raise SonicProxyException('Could not reboot sonic device through '
+ '%s: %s' % (self.SETUP_SERVER_PORT, e))
+
+
+ def stop_app(self, app):
+ """Stops the app.
+
+ Performs a hard reboot if pycurl isn't available.
+
+ @param app: An app name, eg YouTube, Fling, Netflix etc.
+
+ @raises pycurl.error: If the DELETE request fails after retries.
+ """
+ if not pycurl:
+ logging.warning('Rebooting sonic host to stop %s, please install '
+ 'pycurl if you do not wish to reboot.', app)
+ self.reboot()
+ return
+
+ _curl_request(self._hostname, 'apps/%s' % app,
+ self.SONIC_SERVER_PORT, 'DELETE')
+
+
+ def start_app(self, app, payload):
+ """Starts an app.
+
+ @param app: An app name, eg YouTube, Fling, Netflix etc.
+ @param payload: An url payload for the app, eg: http://www.youtube.com.
+
+ @raises error.TimeoutException: If the call times out.
+ """
+ url = '%s/apps/%s' % (self._sonic_server, app)
+ _post(url, payload)
+
diff --git a/server/hosts/factory.py b/server/hosts/factory.py
index 51bbecb..e379134 100644
--- a/server/hosts/factory.py
+++ b/server/hosts/factory.py
@@ -6,7 +6,7 @@
from autotest_lib.client.common_lib import error, global_config
from autotest_lib.server import autotest, utils as server_utils
from autotest_lib.server.hosts import site_factory, cros_host, ssh_host, serial
-from autotest_lib.server.hosts import eureka_host
+from autotest_lib.server.hosts import sonic_host
from autotest_lib.server.hosts import adb_host, logfile_monitor
@@ -30,7 +30,7 @@
# A list of all the possible host types, ordered according to frequency of
# host types in the lab, so the more common hosts don't incur a repeated ssh
# overhead in checking for less common host types.
-host_types = [cros_host.CrosHost, eureka_host.EurekaHost, adb_host.ADBHost,]
+host_types = [cros_host.CrosHost, sonic_host.SonicHost, adb_host.ADBHost,]
def _get_host_arguments():
diff --git a/server/hosts/eureka_host.py b/server/hosts/sonic_host.py
similarity index 63%
rename from server/hosts/eureka_host.py
rename to server/hosts/sonic_host.py
index edd86b9..600acd1 100644
--- a/server/hosts/eureka_host.py
+++ b/server/hosts/sonic_host.py
@@ -3,17 +3,17 @@
# found in the LICENSE file.
"""
-Eureka host.
+Sonic host.
This host can perform actions either over ssh or by submitting requests to
an http server running on the client. Though the server provides flexibility
and allows us to test things at a modular level, there are times we must
resort to ssh (eg: to reboot into recovery). The server exposes the same stack
-that the chromecast extension needs to communicate with the eureka device, so
-any test involving an eureka host will fail if it cannot submit posts/gets
+that the chromecast extension needs to communicate with the sonic device, so
+any test involving an sonic host will fail if it cannot submit posts/gets
to the server. In cases where we can achieve the same action over ssh or
the rpc server, we choose the rpc server by default, because several existing
-eureka tests do the same.
+sonic tests do the same.
"""
import logging
@@ -21,17 +21,16 @@
import common
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import autotemp
from autotest_lib.client.common_lib import error
from autotest_lib.server import site_utils
-from autotest_lib.server.cros import eureka_client
+from autotest_lib.server.cros import sonic_client_utils
from autotest_lib.server.hosts import abstract_ssh
-class EurekaHost(abstract_ssh.AbstractSSHHost):
- """This class represents a eureka host."""
-
- # Maximum time to wait for the client server to start.
- SERVER_START_TIME = 180
+class SonicHost(abstract_ssh.AbstractSSHHost):
+ """This class represents a sonic host."""
# Maximum time a reboot can take.
REBOOT_TIME = 360
@@ -41,32 +40,67 @@
RECOVERY_DIR = '/cache/recovery'
COMMAND_FILE = os.path.join(RECOVERY_DIR, 'command')
-
@staticmethod
def check_host(host, timeout=10):
"""
- Check if the given host is a eureka host.
+ Check if the given host is a sonic host.
@param host: An ssh host representing a device.
@param timeout: The timeout for the run command.
- @return: True if the host device is eureka.
+ @return: True if the host device is sonic.
@raises AutoservRunError: If the command failed.
@raises AutoservSSHTimeout: Ssh connection has timed out.
"""
try:
- result = host.run('getprop ro.hardware', timeout=timeout)
+ result = host.run('getprop ro.product.device', timeout=timeout)
except (error.AutoservRunError, error.AutoservSSHTimeout):
return False
- return 'eureka' in result.stdout
+ return 'anchovy' in result.stdout
def _initialize(self, hostname, *args, **dargs):
- super(EurekaHost, self)._initialize(hostname=hostname, *args, **dargs)
+ super(SonicHost, self)._initialize(hostname=hostname, *args, **dargs)
- # Eureka devices expose a server that can respond to json over http.
- self.client = eureka_client.EurekaProxy(hostname)
+ # Sonic devices expose a server that can respond to json over http.
+ self.client = sonic_client_utils.SonicProxy(hostname)
+
+
+ def enable_test_extension(self):
+ """Enable a chromecast test extension on the sonic host.
+
+ Appends the extension id to the list of accepted cast
+ extensions, without which the sonic device will fail to
+ respond to any Dial requests submitted by the extension.
+
+ @raises CmdExecutionError: If the expected files are not found
+ on the sonic host.
+ """
+ extension_id = sonic_client_utils.get_extension_id()
+ tempdir = autotemp.tempdir()
+ local_dest = os.path.join(tempdir.name, 'content_shell.sh')
+ remote_src = '/system/usr/bin/content_shell.sh'
+ whitelist_flag = '--extra-cast-extension-ids'
+
+ try:
+ self.run('mount -o rw,remount /system')
+ self.get_file(remote_src, local_dest)
+ with open(local_dest) as f:
+ content = f.read()
+ if extension_id in content:
+ return
+ if whitelist_flag in content:
+ append_str = ',%s' % extension_id
+ else:
+ append_str = ' %s=%s' % (whitelist_flag, extension_id)
+
+ with open(local_dest, 'a') as f:
+ f.write(append_str)
+ self.send_file(local_dest, remote_src)
+ self.reboot()
+ finally:
+ tempdir.clean()
def get_boot_id(self, timeout=60):
@@ -93,37 +127,60 @@
@param base_cmd: The base command to use to confirm that a round
trip ssh works.
"""
- super(EurekaHost, self).ssh_ping(timeout=timeout,
+ super(SonicHost, self).ssh_ping(timeout=timeout,
base_cmd="getprop>/dev/null")
def verify_software(self):
"""Verified that the server on the client device is responding to gets.
- The server on the client device is crucial for the eureka device to
+ The server on the client device is crucial for the sonic device to
communicate with the chromecast extension. Device verify on the whole
consists of verify_(hardware, connectivity and software), ssh
connectivity is verified in the base class' verify_connectivity.
- @raises: EurekaProxyException if the server doesn't respond.
+ @raises: SonicProxyException if the server doesn't respond.
"""
- self.client.get_info()
+ self.client.check_server()
+
+
+ def get_build_number(self, timeout_mins=1):
+ """
+ Gets the build number on the sonic device.
+
+ Since this method is usually called right after a reboot/install,
+ it has retries built in.
+
+ @param timeout_mins: The timeout in minutes.
+
+ @return: The build number of the build on the host.
+
+ @raises TimeoutError: If we're unable to get the build number within
+ the specified timeout.
+ @raises ValueError: If the build number returned isn't an integer.
+ """
+ cmd = 'getprop ro.build.version.incremental'
+ timeout = timeout_mins * 60
+ cmd_result = utils.poll_for_condition(
+ lambda: self.run(cmd, timeout=timeout/10),
+ timeout=timeout, sleep_interval=timeout/10)
+ return int(cmd_result.stdout)
def get_kernel_ver(self):
"""Returns the build number of the build on the device."""
- return self.client.get_build_number()
+ return self.get_build_number()
def reboot(self, timeout=5):
- """Reboot the eureka device by submitting a post to the server."""
+ """Reboot the sonic device by submitting a post to the server."""
# TODO(beeps): crbug.com/318306
current_boot_id = self.get_boot_id()
try:
self.client.reboot()
- except eureka_client.EurekaProxyException as e:
- logging.error('Unable to reboot through the eureka proxy: %s', e)
+ except sonic_client_utils.SonicProxyException as e:
+ logging.error('Unable to reboot through the sonic proxy: %s', e)
return False
self.wait_for_restart(timeout=timeout, old_boot_id=current_boot_id)
@@ -141,7 +198,7 @@
self.run('rm -f /cache/*')
except (error.AutotestRunError, error.AutoservRunError) as e:
logging.warn('Unable to remove /data and /cache %s', e)
- super(EurekaHost, self).cleanup()
+ super(SonicHost, self).cleanup()
def _remount_root(self, permissions):
@@ -199,8 +256,8 @@
def machine_install(self, update_url):
- """Installs a build on the Eureka device."""
- old_build_number = self.client.get_build_number()
+ """Installs a build on the Sonic device."""
+ old_build_number = self.get_build_number()
self._remount_root(permissions='rw')
self._setup_coredump_dirs()
self._setup_for_recovery(update_url)
@@ -209,7 +266,7 @@
self.run('reboot recovery &')
self.wait_for_restart(timeout=self.REBOOT_TIME,
old_boot_id=current_boot_id)
- new_build_number = self.client.get_build_number(self.SERVER_START_TIME)
+ new_build_number = self.get_build_number()
# TODO(beeps): crbug.com/318278
if new_build_number == old_build_number:
diff --git a/server/site_tests/generic_RebootTest/control b/server/site_tests/generic_RebootTest/control
index 95134fd..bcdd172 100644
--- a/server/site_tests/generic_RebootTest/control
+++ b/server/site_tests/generic_RebootTest/control
@@ -19,8 +19,8 @@
creates the appropriate host object, and calls reboot on the host object.
Example usage:
-test_that generic_RebootTest <eureka/cros/beaglobonedevice ip> --board=<board>
-A note about --board: <unless you're emerging eureka sources you
+test_that generic_RebootTest <sonic/cros/beaglobonedevice ip> --board=<board>
+A note about --board: <unless you're emerging sonic sources you
can just use a chromeos board here, as all we need is test_that
from the sysroot>.
diff --git a/server/site_tests/generic_RebootTest/generic_RebootTest.py b/server/site_tests/generic_RebootTest/generic_RebootTest.py
index 8ba3e27..ac125d3 100644
--- a/server/site_tests/generic_RebootTest/generic_RebootTest.py
+++ b/server/site_tests/generic_RebootTest/generic_RebootTest.py
@@ -7,7 +7,7 @@
class generic_RebootTest(test.test):
- """Reboot a device. Should be ran on ADBHosts only."""
+ """Reboot a device."""
version = 1
def run_once(self, host):
diff --git a/server/site_tests/sonic_AppTest/control b/server/site_tests/sonic_AppTest/control
new file mode 100644
index 0000000..47aca29
--- /dev/null
+++ b/server/site_tests/sonic_AppTest/control
@@ -0,0 +1,44 @@
+# 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.common_lib import error
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "sonic_AppTest"
+PURPOSE = "Demonstrate the ability to start the chromecast app."
+CRITERIA = "This test will fail if the app fails to start."
+EXPERIMENTAL = "True"
+TIME = "SHORT"
+TEST_CATEGORY = "General"
+TEST_CLASS = "sonic"
+TEST_TYPE = "server"
+
+DOC = """
+This is a sonic test that will cast a tab.
+
+Tab cast is achieved through desktopui_SonicExtension, which is a client
+test that handles all the extension manipulation necessary to talk to the
+sonic device. Note that the sonic host and the DUT need to be on the same
+subnet.
+
+Usage: test_that <ip of Cros DUT> --board=<boardname of DUT>
+ --args="sonic_hostname=<ip of sonichost>"
+"""
+
+args_dict = utils.args_to_dict(args)
+
+def run(machine):
+ sonic_hostname = args_dict.get('sonic_hostname')
+ if not sonic_hostname:
+ raise error.TestError('Cannot run sonic_AppTest without a sonic host. '
+ 'please specify --args="sonic_hostname=<ip>" with '
+ 'test_that.')
+
+ cros_host = hosts.create_host(machine)
+ sonic_host = hosts.create_host(sonic_hostname)
+ job.run_test('sonic_AppTest', cros_host=cros_host, sonic_host=sonic_host,
+ disable_sysinfo=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/sonic_AppTest/control.netflix b/server/site_tests/sonic_AppTest/control.netflix
new file mode 100644
index 0000000..2c66c3c
--- /dev/null
+++ b/server/site_tests/sonic_AppTest/control.netflix
@@ -0,0 +1,40 @@
+# 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.common_lib import error
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "sonic_AppTest_netflix"
+PURPOSE = "Demonstrate the ability to start a sonic app."
+CRITERIA = "This test will fail if the app fails to start."
+EXPERIMENTAL = "True"
+TIME = "SHORT"
+TEST_CATEGORY = "General"
+TEST_CLASS = "sonic"
+TEST_TYPE = "server"
+
+DOC = """
+This is a sonic test that will load the Netflix sonic app.
+
+Usage: test_that <ip of Cros DUT> --board=<boardname of DUT>
+ --args="sonic_hostname=<ip of sonichost>"
+"""
+
+args_dict = utils.args_to_dict(args)
+
+def run(machine):
+ sonic_hostname = args_dict.get('sonic_hostname')
+ if not sonic_hostname:
+ raise error.TestError('Cannot run sonic_AppTest without a sonic host. '
+ 'please specify --args="sonic_hostname=<ip>" with '
+ 'test_that.')
+
+ cros_host = hosts.create_host(machine)
+ sonic_host = hosts.create_host(sonic_hostname)
+ job.run_test('sonic_AppTest', cros_host=cros_host, sonic_host=sonic_host,
+ app='Netflix', payload='http://www.netflix.com',
+ disable_sysinfo=True, tag='netflix')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/sonic_AppTest/sonic_AppTest.py b/server/site_tests/sonic_AppTest/sonic_AppTest.py
new file mode 100644
index 0000000..4d24768
--- /dev/null
+++ b/server/site_tests/sonic_AppTest/sonic_AppTest.py
@@ -0,0 +1,60 @@
+# 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 logging
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.server import autotest
+from autotest_lib.server import test
+
+
+class sonic_AppTest(test.test):
+ """Tests that a sonic device can start its apps."""
+ version = 1
+
+
+ def run_once(self, cros_host, sonic_host, app='ChromeCast', payload=None):
+ """Sonic test to start an app.
+
+ By default this test will test tab cast by installing an extension
+ on the cros host and using chromedriver to cast a tab. If another app
+ is specified, like YouTube or Netflix, the app is tested directly
+ through the server running on port 8080 on the sonic device.
+
+ @param app: The name of the application to start.
+ eg: YouTube
+ @param payload: The payload to send to the app.
+ eg: http://www.youtube.com
+
+ @raises CmdExecutionError: If a command failed to execute on the host.
+ @raises TestError: If the app didn't start, or the app was unrecognized,
+ or the payload is invalid.
+ """
+ sonic_host.run('logcat -c')
+
+ if app == 'ChromeCast':
+ sonic_host.enable_test_extension()
+ client_at = autotest.Autotest(cros_host)
+ client_at.run_test('desktopui_SonicExtension',
+ chromecast_ip=sonic_host.hostname)
+ elif payload and (app == 'Netflix' or app == 'YouTube'):
+ sonic_host.client.start_app(app, payload)
+ else:
+ raise error.TestError('Cannot start app %s with payload %s' %
+ (app, payload))
+
+ log = sonic_host.run('logcat -d').stdout
+ app_started_confirmation = 'App started:'
+ for line in log.split('\n'):
+ if app_started_confirmation in line:
+ logging.info('Successfully started app: %s', line)
+ break
+ else:
+ logging.error(log)
+ raise error.TestError('App %s failed to start' % app)
+
+
+ def cleanup(self, cros_host, sonic_host, app='ChromeCast'):
+ sonic_host.client.stop_app(app)
+