diff options
author | michaelbai@google.com <michaelbai@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-24 18:59:20 +0000 |
---|---|---|
committer | michaelbai@google.com <michaelbai@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-24 18:59:20 +0000 |
commit | 55ee7f4d793315a8a4225bd43309fad1ef3e980a (patch) | |
tree | 68c1f48e940f1bc2b225a53e6a332009c3d21850 /build | |
parent | 84baeca622c2d87c6a0aaf86e7a8362efe56fb25 (diff) | |
download | chromium_src-55ee7f4d793315a8a4225bd43309fad1ef3e980a.zip chromium_src-55ee7f4d793315a8a4225bd43309fad1ef3e980a.tar.gz chromium_src-55ee7f4d793315a8a4225bd43309fad1ef3e980a.tar.bz2 |
Upstream: Test scripts for Android (phase 2)
Currently only support run base_unittests
BUG=
TEST=
Review URL: http://codereview.chromium.org/8364020
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@106953 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build')
-rwxr-xr-x | build/android/android_commands.py | 1 | ||||
-rw-r--r-- | build/android/base_test_runner.py | 147 | ||||
-rw-r--r-- | build/android/chrome_test_server_spawner.py | 114 | ||||
-rw-r--r-- | build/android/flag_changer.py | 50 | ||||
-rw-r--r-- | build/android/gtest_filter/base_unittests_disabled | 8 | ||||
-rw-r--r-- | build/android/gtest_filter/base_unittests_emulator_additional_disabled | 10 | ||||
-rwxr-xr-x | build/android/lighttpd_server.py | 234 | ||||
-rwxr-xr-x | build/android/run_tests.py | 208 | ||||
-rw-r--r-- | build/android/run_tests_helper.py | 134 | ||||
-rwxr-xr-x | build/android/single_test_runner.py | 315 | ||||
-rw-r--r-- | build/android/test_package.py | 164 | ||||
-rw-r--r-- | build/android/test_package_executable.py | 153 | ||||
-rw-r--r-- | build/android/test_result.py | 107 |
13 files changed, 1644 insertions, 1 deletions
diff --git a/build/android/android_commands.py b/build/android/android_commands.py index 622e5ef..27c1c3d 100755 --- a/build/android/android_commands.py +++ b/build/android/android_commands.py @@ -773,5 +773,4 @@ def main(argv): if __name__ == '__main__': - print os.path.abspath(os.path.dirname(__file__)) main(sys.argv) diff --git a/build/android/base_test_runner.py b/build/android/base_test_runner.py new file mode 100644 index 0000000..b136d8d --- /dev/null +++ b/build/android/base_test_runner.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium 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 +import os + +import android_commands +from chrome_test_server_spawner import SpawningServer +from flag_changer import FlagChanger +import lighttpd_server +import run_tests_helper + +FORWARDER_PATH = '/data/local/tmp/forwarder' +# These ports must match up with the constants in net/test/test_server.cc +TEST_SERVER_SPAWNER_PORT = 8001 +TEST_SERVER_PORT = 8002 +TEST_SYNC_SERVER_PORT = 8003 + + +class BaseTestRunner(object): + """Base class for running tests on a single device.""" + + def __init__(self, device): + """ + Args: + device: Tests will run on the device of this ID. + """ + self.device = device + self.adb = android_commands.AndroidCommands(device=device) + # Synchronize date/time between host and device. Otherwise same file on + # host and device may have different timestamp which may cause + # AndroidCommands.PushIfNeeded failed, or a test which may compare timestamp + # got from http head and local time could be failed. + self.adb.SynchronizeDateTime() + self._http_server = None + self._forwarder = None + self._spawning_server = None + self._spawner_forwarder = None + self._forwarder_device_port = 8000 + self._forwarder_base_url = ('http://localhost:%d' % + self._forwarder_device_port) + self._flags = FlagChanger(self.adb) + + def RunTests(self): + # TODO(bulach): this should actually do SetUp / RunTestsInternal / TearDown. + # Refactor the various subclasses to expose a RunTestsInternal without + # any params. + raise NotImplementedError + + def SetUp(self): + """Called before tests run.""" + pass + + def TearDown(self): + """Called when tests finish running.""" + self.ShutdownHelperToolsForTestSuite() + + def CopyTestData(self, test_data_paths, dest_dir): + """Copies |test_data_paths| list of files/directories to |dest_dir|. + + Args: + test_data_paths: A list of files or directories relative to |dest_dir| + which should be copied to the device. The paths must exist in + |CHROME_DIR|. + dest_dir: Absolute path to copy to on the device. + """ + for p in test_data_paths: + self.adb.PushIfNeeded( + os.path.join(run_tests_helper.CHROME_DIR, p), + os.path.join(dest_dir, p)) + + def LaunchTestHttpServer(self, document_root, extra_config_contents=None): + """Launches an HTTP server to serve HTTP tests. + + Args: + document_root: Document root of the HTTP server. + extra_config_contents: Extra config contents for the HTTP server. + """ + self._http_server = lighttpd_server.LighttpdServer( + document_root, extra_config_contents=extra_config_contents) + if self._http_server.StartupHttpServer(): + logging.info('http server started: http://localhost:%s', + self._http_server.port) + else: + logging.critical('Failed to start http server') + # Root access needed to make the forwarder executable work. + self.adb.EnableAdbRoot() + self.StartForwarderForHttpServer() + + def StartForwarderForHttpServer(self): + """Starts a forwarder for the HTTP server. + + The forwarder forwards HTTP requests and responses between host and device. + """ + # Sometimes the forwarder device port may be already used. We have to kill + # all forwarder processes to ensure that the forwarder can be started since + # currently we can not associate the specified port to related pid. + # TODO(yfriedman/wangxianzhu): This doesn't work as most of the time the + # port is in use but the forwarder is already dead. Killing all forwarders + # is overly destructive and breaks other tests which make use of forwarders. + # if IsDevicePortUsed(self.adb, self._forwarder_device_port): + # self.adb.KillAll('forwarder') + self._forwarder = run_tests_helper.ForwardDevicePorts( + self.adb, [(self._forwarder_device_port, self._http_server.port)]) + + def RestartHttpServerForwarderIfNecessary(self): + """Restarts the forwarder if it's not open.""" + # Checks to see if the http server port is being used. If not forwards the + # request. + # TODO(dtrainor): This is not always reliable because sometimes the port + # will be left open even after the forwarder has been killed. + if not run_tests_helper.IsDevicePortUsed(self.adb, + self._forwarder_device_port): + self.StartForwarderForHttpServer() + + def ShutdownHelperToolsForTestSuite(self): + """Shuts down the server and the forwarder.""" + # Forwarders should be killed before the actual servers they're forwarding + # to as they are clients potentially with open connections and to allow for + # proper hand-shake/shutdown. + if self._forwarder or self._spawner_forwarder: + # Kill all forwarders on the device and then kill the process on the host + # (if it exists) + self.adb.KillAll('forwarder') + if self._forwarder: + self._forwarder.kill() + if self._spawner_forwarder: + self._spawner_forwarder.kill() + if self._http_server: + self._http_server.ShutdownHttpServer() + if self._spawning_server: + self._spawning_server.Stop() + self._flags.Restore() + + def LaunchChromeTestServerSpawner(self): + """Launches test server spawner.""" + self._spawning_server = SpawningServer(TEST_SERVER_SPAWNER_PORT, + TEST_SERVER_PORT) + self._spawning_server.Start() + # TODO(yfriedman): Ideally we'll only try to start up a port forwarder if + # there isn't one already running but for now we just get an error message + # and the existing forwarder still works. + self._spawner_forwarder = run_tests_helper.ForwardDevicePorts( + self.adb, [(TEST_SERVER_SPAWNER_PORT, TEST_SERVER_SPAWNER_PORT), + (TEST_SERVER_PORT, TEST_SERVER_PORT)]) diff --git a/build/android/chrome_test_server_spawner.py b/build/android/chrome_test_server_spawner.py new file mode 100644 index 0000000..85864dada --- /dev/null +++ b/build/android/chrome_test_server_spawner.py @@ -0,0 +1,114 @@ +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""A "Test Server Spawner" that handles killing/stopping per-test test servers. + +It's used to accept requests from the device to spawn and kill instances of the +chrome test server on the host. +""" + +import BaseHTTPServer +import logging +import os +import sys +import threading +import time +import urlparse + +# Path that are needed to import testserver +cr_src = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..') +sys.path.append(os.path.join(cr_src, 'third_party')) +sys.path.append(os.path.join(cr_src, 'third_party', 'tlslite')) +sys.path.append(os.path.join(cr_src, 'third_party', 'pyftpdlib', 'src')) +sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', + '..', 'net', 'tools', 'testserver')) +import testserver + +_test_servers = [] + +class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """A handler used to process http GET request. + """ + + def GetServerType(self, server_type): + """Returns the server type to use when starting the test server. + + This function translate the command-line argument into the appropriate + numerical constant. + # TODO(yfriedman): Do that translation! + """ + if server_type: + pass + return 0 + + def do_GET(self): + parsed_path = urlparse.urlparse(self.path) + action = parsed_path.path + params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) + logging.info('Action is: %s' % action) + if action == '/killserver': + # There should only ever be one test server at a time. This may do the + # wrong thing if we try and start multiple test servers. + _test_servers.pop().Stop() + elif action == '/start': + logging.info('Handling request to spawn a test webserver') + for param in params: + logging.info('%s=%s' % (param, params[param][0])) + s_type = 0 + doc_root = None + if 'server_type' in params: + s_type = self.GetServerType(params['server_type'][0]) + if 'doc_root' in params: + doc_root = params['doc_root'][0] + self.webserver_thread = threading.Thread( + target=self.SpawnTestWebServer, args=(s_type, doc_root)) + self.webserver_thread.setDaemon(True) + self.webserver_thread.start() + self.send_response(200, 'OK') + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write('<html><head><title>started</title></head></html>') + logging.info('Returned OK!!!') + + def SpawnTestWebServer(self, s_type, doc_root): + class Options(object): + log_to_console = True + server_type = s_type + port = self.server.test_server_port + data_dir = doc_root or 'chrome/test/data' + file_root_url = '/files/' + cert = False + policy_keys = None + policy_user = None + startup_pipe = None + options = Options() + logging.info('Listening on %d, type %d, data_dir %s' % (options.port, + options.server_type, options.data_dir)) + testserver.main(options, None, server_list=_test_servers) + logging.info('Test-server has died.') + + +class SpawningServer(object): + """The class used to start/stop a http server. + """ + + def __init__(self, test_server_spawner_port, test_server_port): + logging.info('Creating new spawner %d', test_server_spawner_port) + self.server = testserver.StoppableHTTPServer(('', test_server_spawner_port), + SpawningServerRequestHandler) + self.port = test_server_spawner_port + self.server.test_server_port = test_server_port + + def Listen(self): + logging.info('Starting test server spawner') + self.server.serve_forever() + + def Start(self): + listener_thread = threading.Thread(target=self.Listen) + listener_thread.setDaemon(True) + listener_thread.start() + time.sleep(1) + + def Stop(self): + self.server.Stop() diff --git a/build/android/flag_changer.py b/build/android/flag_changer.py new file mode 100644 index 0000000..636022b --- /dev/null +++ b/build/android/flag_changer.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# Location where chrome reads command line flags from +CHROME_COMMAND_FILE = '/data/local/chrome-command-line' + + +class FlagChanger(object): + """Temporarily changes the flags Chrome runs with.""" + + def __init__(self, android_cmd): + self._android_cmd = android_cmd + self._old_flags = None + + def Set(self, flags, append=False): + """Sets the command line flags used when chrome is started. + + Args: + flags: A list of flags to set, eg. ['--single-process']. + append: Whether to append to existing flags or overwrite them. + """ + if flags: + assert flags[0] != 'chrome' + + if not self._old_flags: + self._old_flags = self._android_cmd.GetFileContents(CHROME_COMMAND_FILE) + if self._old_flags: + self._old_flags = self._old_flags[0].strip() + + if append and self._old_flags: + # Avoid appending flags that are already present. + new_flags = filter(lambda flag: self._old_flags.find(flag) == -1, flags) + self._android_cmd.SetFileContents(CHROME_COMMAND_FILE, + self._old_flags + ' ' + + ' '.join(new_flags)) + else: + self._android_cmd.SetFileContents(CHROME_COMMAND_FILE, + 'chrome ' + ' '.join(flags)) + + def Restore(self): + """Restores the flags to their original state.""" + if self._old_flags == None: + return # Set() was never called. + elif self._old_flags: + self._android_cmd.SetFileContents(CHROME_COMMAND_FILE, self._old_flags) + else: + self._android_cmd.RunShellCommand('rm ' + CHROME_COMMAND_FILE) diff --git a/build/android/gtest_filter/base_unittests_disabled b/build/android/gtest_filter/base_unittests_disabled new file mode 100644 index 0000000..1ff27b2 --- /dev/null +++ b/build/android/gtest_filter/base_unittests_disabled @@ -0,0 +1,8 @@ +# List of suppressions +# +# Automatically generated by run_tests.py +RTLTest.GetTextDirection +ReadOnlyFileUtilTest.ContentsEqual +ReadOnlyFileUtilTest.TextContentsEqual +SharedMemoryTest.OpenExclusive +StackTrace.DebugPrintBacktrace diff --git a/build/android/gtest_filter/base_unittests_emulator_additional_disabled b/build/android/gtest_filter/base_unittests_emulator_additional_disabled new file mode 100644 index 0000000..85e8fd6 --- /dev/null +++ b/build/android/gtest_filter/base_unittests_emulator_additional_disabled @@ -0,0 +1,10 @@ +# Addtional list of suppressions from emulator +# +# Automatically generated by run_tests.py +PathServiceTest.Get +SharedMemoryTest.OpenClose +StringPrintfTest.StringAppendfInt +StringPrintfTest.StringAppendfString +StringPrintfTest.StringPrintfBounds +StringPrintfTest.StringPrintfMisc +VerifyPathControlledByUserTest.Symlinks diff --git a/build/android/lighttpd_server.py b/build/android/lighttpd_server.py new file mode 100755 index 0000000..45bfcd0 --- /dev/null +++ b/build/android/lighttpd_server.py @@ -0,0 +1,234 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Provides a convenient wrapper for spawning a test lighttpd instance. + +Usage: + lighttpd_server PATH_TO_DOC_ROOT +""" + +import codecs +import contextlib +import httplib +import os +import pexpect +import random +import shutil +import socket +import sys +import tempfile + + +class LighttpdServer(object): + """Wraps lighttpd server, providing robust startup. + + Args: + document_root: Path to root of this server's hosted files. + port: TCP port on the _host_ machine that the server will listen on. If + ommitted it will attempt to use 9000, or if unavailable it will find + a free port from 8001 - 8999. + lighttpd_path, lighttpd_module_path: Optional paths to lighttpd binaries. + base_config_path: If supplied this file will replace the built-in default + lighttpd config file. + extra_config_contents: If specified, this string will be appended to the + base config (default built-in, or from base_config_path). + config_path, error_log, access_log: Optional paths where the class should + place temprary files for this session. + """ + + def __init__(self, document_root, port=None, + lighttpd_path=None, lighttpd_module_path=None, + base_config_path=None, extra_config_contents=None, + config_path=None, error_log=None, access_log=None): + self.temp_dir = tempfile.mkdtemp(prefix='lighttpd_for_chrome_android') + self.document_root = os.path.abspath(document_root) + self.fixed_port = port + self.port = port or 9000 + self.server_tag = 'LightTPD ' + str(random.randint(111111, 999999)) + self.lighttpd_path = lighttpd_path or '/usr/sbin/lighttpd' + self.lighttpd_module_path = lighttpd_module_path or '/usr/lib/lighttpd' + self.base_config_path = base_config_path + self.extra_config_contents = extra_config_contents + self.config_path = config_path or self._Mktmp('config') + self.error_log = error_log or self._Mktmp('error_log') + self.access_log = access_log or self._Mktmp('access_log') + self.pid_file = self._Mktmp('pid_file') + self.process = None + + def _Mktmp(self, name): + return os.path.join(self.temp_dir, name) + + def _GetRandomPort(self): + # Ports 8001-8004 are reserved for other test servers. Ensure we don't + # collide with them. + return random.randint(8005, 8999) + + def StartupHttpServer(self): + """Starts up a http server with specified document root and port.""" + # Currently we use lighttpd as http sever in test. + while True: + if self.base_config_path: + # Read the config + with codecs.open(self.base_config_path, 'r', 'utf-8') as f: + config_contents = f.read() + else: + config_contents = self._GetDefaultBaseConfig() + if self.extra_config_contents: + config_contents += self.extra_config_contents + # Write out the config, filling in placeholders from the members of |self| + with codecs.open(self.config_path, 'w', 'utf-8') as f: + f.write(config_contents % self.__dict__) + if (not os.path.exists(self.lighttpd_path) or + not os.access(self.lighttpd_path, os.X_OK)): + raise EnvironmentError( + 'Could not find lighttpd at %s.\n' + 'It may need to be installed (e.g. sudo apt-get install lighttpd)' + % self.lighttpd_path) + self.process = pexpect.spawn(self.lighttpd_path, + ['-D', '-f', self.config_path, + '-m', self.lighttpd_module_path], + cwd=self.temp_dir) + client_error, server_error = self._TestServerConnection() + if not client_error: + assert int(open(self.pid_file, 'r').read()) == self.process.pid + break + self.process.close() + + if self.fixed_port or not 'in use' in server_error: + print 'Client error:', client_error + print 'Server error:', server_error + return False + self.port = self._GetRandomPort() + return True + + def ShutdownHttpServer(self): + """Shuts down our lighttpd processes.""" + if self.process: + self.process.terminate() + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def _TestServerConnection(self): + # Wait for server to start + server_msg = '' + for timeout in xrange(1, 5): + client_error = None + try: + with contextlib.closing(httplib.HTTPConnection( + '127.0.0.1', self.port, timeout=timeout)) as http: + http.set_debuglevel(timeout > 3) + http.request('HEAD', '/') + r = http.getresponse() + r.read() + if (r.status == 200 and r.reason == 'OK' and + r.getheader('Server') == self.server_tag): + return (None, server_msg) + client_error = ('Bad response: %s %s version %s\n ' % + (r.status, r.reason, r.version) + + '\n '.join([': '.join(h) for h in r.getheaders()])) + except (httplib.HTTPException, socket.error) as client_error: + pass # Probably too quick connecting: try again + # Check for server startup error messages + ix = self.process.expect([pexpect.TIMEOUT, pexpect.EOF, '.+'], + timeout=timeout) + if ix == 2: # stdout spew from the server + server_msg += self.process.match.group(0) + elif ix == 1: # EOF -- server has quit so giveup. + client_error = client_error or 'Server exited' + break + return (client_error or 'Timeout', server_msg) + + def _GetDefaultBaseConfig(self): + return """server.tag = "%(server_tag)s" +server.modules = ( "mod_access", + "mod_accesslog", + "mod_alias", + "mod_cgi", + "mod_rewrite" ) + +# default document root required +#server.document-root = "." + +# files to check for if .../ is requested +index-file.names = ( "index.php", "index.pl", "index.cgi", + "index.html", "index.htm", "default.htm" ) +# mimetype mapping +mimetype.assign = ( + ".gif" => "image/gif", + ".jpg" => "image/jpeg", + ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".svg" => "image/svg+xml", + ".css" => "text/css", + ".html" => "text/html", + ".htm" => "text/html", + ".xhtml" => "application/xhtml+xml", + ".xhtmlmp" => "application/vnd.wap.xhtml+xml", + ".js" => "application/x-javascript", + ".log" => "text/plain", + ".conf" => "text/plain", + ".text" => "text/plain", + ".txt" => "text/plain", + ".dtd" => "text/xml", + ".xml" => "text/xml", + ".manifest" => "text/cache-manifest", + ) + +# Use the "Content-Type" extended attribute to obtain mime type if possible +mimetype.use-xattr = "enable" + +## +# which extensions should not be handle via static-file transfer +# +# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi +static-file.exclude-extensions = ( ".php", ".pl", ".cgi" ) + +server.bind = "127.0.0.1" +server.port = %(port)s + +## virtual directory listings +dir-listing.activate = "enable" +#dir-listing.encoding = "iso-8859-2" +#dir-listing.external-css = "style/oldstyle.css" + +## enable debugging +#debug.log-request-header = "enable" +#debug.log-response-header = "enable" +#debug.log-request-handling = "enable" +#debug.log-file-not-found = "enable" + +#### SSL engine +#ssl.engine = "enable" +#ssl.pemfile = "server.pem" + +# Autogenerated test-specific config follows. + +cgi.assign = ( ".cgi" => "/usr/bin/env", + ".pl" => "/usr/bin/env", + ".asis" => "/bin/cat", + ".php" => "/usr/bin/php-cgi" ) + +server.errorlog = "%(error_log)s" +accesslog.filename = "%(access_log)s" +server.upload-dirs = ( "/tmp" ) +server.pid-file = "%(pid_file)s" +server.document-root = "%(document_root)s" + +""" + + +def main(argv): + server = LighttpdServer(*argv[1:]) + try: + if server.StartupHttpServer(): + raw_input('Server running at http://127.0.0.1:%s -' + ' press Enter to exit it.' % server.port) + else: + print 'Server exit code:', server.process.exitstatus + finally: + server.ShutdownHttpServer() + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/build/android/run_tests.py b/build/android/run_tests.py new file mode 100755 index 0000000..1425240 --- /dev/null +++ b/build/android/run_tests.py @@ -0,0 +1,208 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Runs all the native unit tests. + +1. Copy over test binary to /data/local on device. +2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak) + to be deployed to the device (in /data/local/tmp). +3. Environment: +3.1. chrome/unit_tests requires (via chrome_paths.cc) a directory named: + /data/local/tmp/chrome/test/data +3.2. page_cycler_tests have following requirements, +3.2.1 the following data on host: + <chrome_src_dir>/tools/page_cycler + <chrome_src_dir>/data/page_cycler +3.2.2. two data directories to store above test data on device named: + /data/local/tmp/tools/ (for database perf test) + /data/local/tmp/data/ (for other perf tests) +3.2.3. a http server to serve http perf tests. + The http root is host's <chrome_src_dir>/data/page_cycler/, port 8000. +3.2.4 a tool named forwarder is also required to run on device to + forward the http request/response between host and device. +3.2.5 Chrome is installed on device. +4. Run the binary in the device and stream the log to the host. +4.1. Optionally, filter specific tests. +4.2. Optionally, rebaseline: run the available tests and update the + suppressions file for failures. +4.3. If we're running a single test suite and we have multiple devices + connected, we'll shard the tests. +5. Clean up the device. + +Suppressions: + +Individual tests in a test binary can be suppressed by listing it in +the gtest_filter directory in a file of the same name as the test binary, +one test per line. Here is an example: + + $ cat gtest_filter/base_unittests_disabled + DataPackTest.Load + ReadOnlyFileUtilTest.ContentsEqual + +This file is generated by the tests running on devices. If running on emulator, +additonal filter file which lists the tests only failed in emulator will be +loaded. We don't care about the rare testcases which succeeded on emuatlor, but +failed on device. +""" + +import logging +import os +import re +import sys + +import android_commands +import cmd_helper +import debug_info +import emulator +import run_tests_helper +from single_test_runner import SingleTestRunner +from test_package_executable import TestPackageExecutable +from test_result import BaseTestResult, TestResults + +_TEST_SUITES = ['base_unittests',] + +def RunTests(device, test_suite, gtest_filter, test_arguments, rebaseline, + timeout, performance_test, cleanup_test_files, tool, + log_dump_name): + """Runs the tests. + + Args: + device: Device to run the tests. + test_suite: A specific test suite to run, empty to run all. + gtest_filter: A gtest_filter flag. + test_arguments: Additional arguments to pass to the test binary. + rebaseline: Whether or not to run tests in isolation and update the filter. + timeout: Timeout for each test. + performance_test: Whether or not performance test(s). + cleanup_test_files: Whether or not to cleanup test files on device. + tool: Name of the Valgrind tool. + log_dump_name: Name of log dump file. + + Returns: + A TestResults object. + """ + results = [] + + if test_suite: + global _TEST_SUITES + if not os.path.exists(test_suite): + logging.critical('Unrecognized test suite, supported: %s' % + _TEST_SUITES) + if test_suite in _TEST_SUITES: + logging.critical('(Remember to include the path: out/Release/%s)', + test_suite) + return TestResults.FromOkAndFailed([], [BaseTestResult(test_suite, '')]) + _TEST_SUITES = [test_suite] + else: + # If not specified, assume the test suites are in out/Release + test_suite_dir = os.path.abspath(os.path.join(run_tests_helper.CHROME_DIR, + 'out', 'Release')) + _TEST_SUITES = [os.path.join(test_suite_dir, t) for t in _TEST_SUITES] + debug_info_list = [] + for t in _TEST_SUITES: + test = SingleTestRunner(device, t, gtest_filter, test_arguments, + timeout, rebaseline, performance_test, + cleanup_test_files, tool, not not log_dump_name) + test.RunTests() + results += [test.test_results] + # Collect debug info. + debug_info_list += [test.dump_debug_info] + if rebaseline: + test.UpdateFilter(test.test_results.failed) + elif test.test_results.failed: + # Stop running test if encountering failed test. + test.test_results.LogFull() + break + # Zip all debug info outputs into a file named by log_dump_name. + debug_info.GTestDebugInfo.ZipAndCleanResults( + os.path.join(run_tests_helper.CHROME_DIR, 'out', 'Release', + 'debug_info_dumps'), + log_dump_name, [d for d in debug_info_list if d]) + return TestResults.FromTestResults(results) + +def Dispatch(options): + """Dispatches the tests, sharding if possible. + + If options.use_emulator is True, all tests will be run in a new emulator + instance. + + Args: + options: options for running the tests. + + Returns: + 0 if successful, number of failing tests otherwise. + """ + if options.test_suite == 'help': + ListTestSuites() + return 0 + buildbot_emulator = None + attached_devices = [] + + if options.use_emulator: + buildbot_emulator = emulator.Emulator() + buildbot_emulator.Launch() + attached_devices.append(buildbot_emulator.device) + else: + attached_devices = android_commands.GetAttachedDevices() + + if not attached_devices: + logging.critical('A device must be attached and online.') + return 1 + + test_results = RunTests(attached_devices[0], options.test_suite, + options.gtest_filter, options.test_arguments, + options.rebaseline, options.timeout, + options.performance_test, + options.cleanup_test_files, options.tool, + options.log_dump) + if buildbot_emulator: + buildbot_emulator.Shutdown() + return len(test_results.failed) + +def ListTestSuites(): + """Display a list of available test suites + """ + print 'Available test suites are:' + for test_suite in _TEST_SUITES: + print test_suite + + +def main(argv): + option_parser = run_tests_helper.CreateTestRunnerOptionParser(None, + default_timeout=0) + option_parser.add_option('-s', dest='test_suite', + help='Executable name of the test suite to run ' + '(use -s help to list them)') + option_parser.add_option('-r', dest='rebaseline', + help='Rebaseline and update *testsuite_disabled', + action='store_true', + default=False) + option_parser.add_option('-f', dest='gtest_filter', + help='gtest filter') + option_parser.add_option('-a', '--test_arguments', dest='test_arguments', + help='Additional arguments to pass to the test') + option_parser.add_option('-p', dest='performance_test', + help='Indicator of performance test', + action='store_true', + default=False) + option_parser.add_option('-L', dest='log_dump', + help='file name of log dump, which will be put in' + 'subfolder debug_info_dumps under the same directory' + 'in where the test_suite exists.') + option_parser.add_option('-e', '--emulator', dest='use_emulator', + help='Run tests in a new instance of emulator', + action='store_true', + default=False) + options, args = option_parser.parse_args(argv) + if len(args) > 1: + print 'Unknown argument:', args[1:] + option_parser.print_usage() + sys.exit(1) + run_tests_helper.SetLogLevel(options.verbose_count) + return Dispatch(options) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/build/android/run_tests_helper.py b/build/android/run_tests_helper.py new file mode 100644 index 0000000..dcdc8b2 --- /dev/null +++ b/build/android/run_tests_helper.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Helper functions common to native test runners.""" + +import logging +import optparse +import os +import subprocess +import sys + +# TODO(michaelbai): Move constant definitions like below to a common file. +FORWARDER_PATH = '/data/local/tmp/forwarder' + +CHROME_DIR = os.path.abspath(os.path.join(sys.path[0], '..', '..')) + + +def IsRunningAsBuildbot(): + """Returns True if we are currently running on buildbot; False otherwise.""" + return bool(os.getenv('BUILDBOT_BUILDERNAME')) + + +def ReportBuildbotLink(label, url): + """Adds a link with name |label| linking to |url| to current buildbot step. + + Args: + label: A string with the name of the label. + url: A string of the URL. + """ + if IsRunningAsBuildbot(): + print '@@@STEP_LINK@%s@%s@@@' % (label, url) + + +def ReportBuildbotMsg(msg): + """Appends |msg| to the current buildbot step text. + + Args: + msg: String to be appended. + """ + if IsRunningAsBuildbot(): + print '@@@STEP_TEXT@%s@@@' % msg + +def ReportBuildbotError(): + """Marks the current step as failed.""" + if IsRunningAsBuildbot(): + print '@@@STEP_FAILURE@@@' + + +def GetExpectations(file_name): + """Returns a list of test names in the |file_name| test expectations file.""" + if not file_name or not os.path.exists(file_name): + return [] + return [x for x in [x.strip() for x in file(file_name).readlines()] + if x and x[0] != '#'] + + +def SetLogLevel(verbose_count): + """Sets log level as |verbose_count|.""" + log_level = logging.WARNING # Default. + if verbose_count == 1: + log_level = logging.INFO + elif verbose_count >= 2: + log_level = logging.DEBUG + logging.getLogger().setLevel(log_level) + + +def CreateTestRunnerOptionParser(usage=None, default_timeout=60): + """Returns a new OptionParser with arguments applicable to all tests.""" + option_parser = optparse.OptionParser(usage=usage) + option_parser.add_option('-t', dest='timeout', + help='Timeout to wait for each test', + type='int', + default=default_timeout) + option_parser.add_option('-c', dest='cleanup_test_files', + help='Cleanup test files on the device after run', + action='store_true', + default=False) + option_parser.add_option('-v', + '--verbose', + dest='verbose_count', + default=0, + action='count', + help='Verbose level (multiple times for more)') + option_parser.add_option('--tool', + dest='tool', + help='Run the test under a tool ' + '(use --tool help to list them)') + return option_parser + + +def ForwardDevicePorts(adb, ports, host_name='127.0.0.1'): + """Forwards a TCP port on the device back to the host. + + Works like adb forward, but in reverse. + + Args: + adb: Instance of AndroidCommands for talking to the device. + ports: A list of tuples (device_port, host_port) to forward. + host_name: Optional. Address to forward to, must be addressable from the + host machine. Usually this is omitted and loopback is used. + + Returns: + subprocess instance connected to the forwarder process on the device. + """ + adb.PushIfNeeded( + os.path.join(CHROME_DIR, 'out', 'Release', 'forwarder'), FORWARDER_PATH) + forward_string = ['%d:%d:%s' % + (device, host, host_name) for device, host in ports] + logging.info("Forwarding ports: %s" % (forward_string)) + + return subprocess.Popen( + ['adb', '-s', adb._adb.GetSerialNumber(), + 'shell', '%s -D %s' % (FORWARDER_PATH, ' '.join(forward_string))]) + + +def IsDevicePortUsed(adb, device_port): + """Checks whether the specified device port is used or not. + + Args: + adb: Instance of AndroidCommands for talking to the device. + device_port: Port on device we want to check. + + Returns: + True if the port on device is already used, otherwise returns False. + """ + base_url = '127.0.0.1:%d' % device_port + netstat_results = adb.RunShellCommand('netstat') + for single_connect in netstat_results: + # Column 3 is the local address which we want to check with. + if single_connect.split()[3] == base_url: + return True + return False diff --git a/build/android/single_test_runner.py b/build/android/single_test_runner.py new file mode 100755 index 0000000..c238f5d --- /dev/null +++ b/build/android/single_test_runner.py @@ -0,0 +1,315 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium 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 +import os +import sys + +from base_test_runner import BaseTestRunner +import debug_info +import run_tests_helper +from test_package_executable import TestPackageExecutable +from test_result import TestResults + + +class SingleTestRunner(BaseTestRunner): + """Single test suite attached to a single device. + + Args: + device: Device to run the tests. + test_suite: A specific test suite to run, empty to run all. + gtest_filter: A gtest_filter flag. + test_arguments: Additional arguments to pass to the test binary. + timeout: Timeout for each test. + rebaseline: Whether or not to run tests in isolation and update the filter. + performance_test: Whether or not performance test(s). + cleanup_test_files: Whether or not to cleanup test files on device. + tool: Name of the Valgrind tool. + dump_debug_info: Whether or not to dump debug information. + """ + + def __init__(self, device, test_suite, gtest_filter, test_arguments, timeout, + rebaseline, performance_test, cleanup_test_files, tool, + dump_debug_info=False): + BaseTestRunner.__init__(self, device) + self._running_on_emulator = self.device.startswith('emulator') + self._gtest_filter = gtest_filter + self._test_arguments = test_arguments + self.test_results = TestResults() + if dump_debug_info: + self.dump_debug_info = debug_info.GTestDebugInfo(self.adb, device, + os.path.basename(test_suite), gtest_filter) + else: + self.dump_debug_info = None + + self.test_package = TestPackageExecutable(self.adb, device, + test_suite, timeout, rebaseline, performance_test, cleanup_test_files, + tool, self.dump_debug_info) + + def _GetHttpServerDocumentRootForTestSuite(self): + """Returns the document root needed by the test suite.""" + if self.test_package.test_suite_basename == 'page_cycler_tests': + return os.path.join(run_tests_helper.CHROME_DIR, 'data', 'page_cycler') + return None + + + def _TestSuiteRequiresMockTestServer(self): + """Returns True if the test suite requires mock test server.""" + return False + # TODO(yfriedman): Disabled because of flakiness. + # (self.test_package.test_suite_basename == 'unit_tests' or + # self.test_package.test_suite_basename == 'net_unittests' or + # False) + + def _GetFilterFileName(self): + """Returns the filename of gtest filter.""" + filter_dir = os.path.join(sys.path[0], 'gtest_filter') + filter_name = self.test_package.test_suite_basename + '_disabled' + disabled_filter = os.path.join(filter_dir, filter_name) + return disabled_filter + + def _GetAdditionalEmulatorFilterName(self): + """Returns the filename of additional gtest filter for emulator.""" + filter_dir = os.path.join(sys.path[0], 'gtest_filter') + filter_name = '%s%s' % (self.test_package.test_suite_basename, + '_emulator_additional_disabled') + disabled_filter = os.path.join(filter_dir, filter_name) + return disabled_filter + + def GetDisabledTests(self): + """Returns a list of disabled tests. + + Returns: + A list of disabled tests obtained from gtest_filter/test_suite_disabled. + """ + disabled_tests = run_tests_helper.GetExpectations(self._GetFilterFileName()) + if self._running_on_emulator: + # Append emulator's filter file. + disabled_tests.extend(run_tests_helper.GetExpectations( + self._GetAdditionalEmulatorFilterName())) + return disabled_tests + + def UpdateFilter(self, failed_tests): + """Updates test_suite_disabled file with the new filter (deletes if empty). + + If running in Emulator, only the failed tests which are not in the normal + filter returned by _GetFilterFileName() are written to emulator's + additional filter file. + + Args: + failed_tests: A sorted list of failed tests. + """ + disabled_tests = [] + if not self._running_on_emulator: + filter_file_name = self._GetFilterFileName() + else: + filter_file_name = self._GetAdditionalEmulatorFilterName() + disabled_tests.extend( + run_tests_helper.GetExpectations(self._GetFilterFileName())) + logging.info('About to update emulator\'s addtional filter (%s).' + % filter_file_name) + + new_failed_tests = [] + if failed_tests: + for test in failed_tests: + if test.name not in disabled_tests: + new_failed_tests.append(test.name) + + if not new_failed_tests: + if os.path.exists(filter_file_name): + os.unlink(filter_file_name) + return + + filter_file = file(filter_file_name, 'w') + if self._running_on_emulator: + filter_file.write('# Addtional list of suppressions from emulator\n') + else: + filter_file.write('# List of suppressions\n') + filter_file.write("""This file was automatically generated by run_tests.py + """) + filter_file.write('\n'.join(sorted(new_failed_tests))) + filter_file.write('\n') + filter_file.close() + + def GetDataFilesForTestSuite(self): + """Returns a list of data files/dirs needed by the test suite.""" + # Ideally, we'd just push all test data. However, it has >100MB, and a lot + # of the files are not relevant (some are used for browser_tests, others for + # features not supported, etc..). + if self.test_package.test_suite_basename in ['base_unittests', + 'sql_unittests', + 'unit_tests']: + return [ + 'net/data/cache_tests/insert_load1', + 'net/data/cache_tests/dirty_entry5', + 'ui/base/test/data/data_pack_unittest', + 'chrome/test/data/bookmarks/History_with_empty_starred', + 'chrome/test/data/bookmarks/History_with_starred', + 'chrome/test/data/extensions/json_schema_test.js', + 'chrome/test/data/History/', + 'chrome/test/data/json_schema_validator/', + 'chrome/test/data/serializer_nested_test.js', + 'chrome/test/data/serializer_test.js', + 'chrome/test/data/serializer_test_nowhitespace.js', + 'chrome/test/data/top_sites/', + 'chrome/test/data/web_database', + 'chrome/test/data/zip', + ] + elif self.test_package.test_suite_basename == 'net_unittests': + return [ + 'net/data/cache_tests', + 'net/data/filter_unittests', + 'net/data/ftp', + 'net/data/proxy_resolver_v8_unittest', + 'net/data/ssl/certificates', + ] + elif self.test_package.test_suite_basename == 'ui_tests': + return [ + 'chrome/test/data/dromaeo', + 'chrome/test/data/json2.js', + 'chrome/test/data/sunspider', + 'chrome/test/data/v8_benchmark', + 'chrome/test/ui/sunspider_uitest.js', + 'chrome/test/ui/v8_benchmark_uitest.js', + ] + elif self.test_package.test_suite_basename == 'page_cycler_tests': + data = [ + 'tools/page_cycler', + 'data/page_cycler', + ] + for d in data: + if not os.path.exists(d): + raise Exception('Page cycler data not found.') + return data + elif self.test_package.test_suite_basename == 'webkit_unit_tests': + return [ + 'third_party/WebKit/Source/WebKit/chromium/tests/data', + ] + return [] + + def LaunchHelperToolsForTestSuite(self): + """Launches helper tools for the test suite. + + Sometimes one test may need to run some helper tools first in order to + successfully complete the test. + """ + document_root = self._GetHttpServerDocumentRootForTestSuite() + if document_root: + self.LaunchTestHttpServer(document_root) + if self._TestSuiteRequiresMockTestServer(): + self.LaunchChromeTestServerSpawner() + + def StripAndCopyFiles(self): + """Strips and copies the required data files for the test suite.""" + self.test_package.StripAndCopyExecutable() + self.test_package.tool.CopyFiles() + test_data = self.GetDataFilesForTestSuite() + if test_data: + if self.test_package.test_suite_basename == 'page_cycler_tests': + # Since the test data for page cycler are huge (around 200M), we use + # sdcard to store the data and create symbol links to map them to + # data/local/tmp/ later. + self.CopyTestData(test_data, '/sdcard/') + for p in [os.path.dirname(d) for d in test_data if os.path.isdir(d)]: + mapped_device_path = '/data/local/tmp/' + p + # Unlink the mapped_device_path at first in case it was mapped to + # a wrong path. Add option '-r' becuase the old path could be a dir. + self.adb.RunShellCommand('rm -r %s' % mapped_device_path) + self.adb.RunShellCommand( + 'ln -s /sdcard/%s %s' % (p, mapped_device_path)) + else: + self.CopyTestData(test_data, '/data/local/tmp/') + + def RunTestsWithFilter(self): + """Runs a tests via a small, temporary shell script.""" + self.test_package.CreateTestRunnerScript(self._gtest_filter, + self._test_arguments) + self.test_results = self.test_package.RunTestsAndListResults() + + def RebaselineTests(self): + """Runs all available tests, restarting in case of failures.""" + if self._gtest_filter: + all_tests = set(self._gtest_filter.split(':')) + else: + all_tests = set(self.test_package.GetAllTests()) + failed_results = set() + executed_results = set() + while True: + executed_names = set([f.name for f in executed_results]) + self._gtest_filter = ':'.join(all_tests - executed_names) + self.RunTestsWithFilter() + failed_results.update(self.test_results.crashed, + self.test_results.failed) + executed_results.update(self.test_results.crashed, + self.test_results.failed, + self.test_results.ok) + executed_names = set([f.name for f in executed_results]) + logging.info('*' * 80) + logging.info(self.device) + logging.info('Executed: ' + str(len(executed_names)) + ' of ' + + str(len(all_tests))) + logging.info('Failed so far: ' + str(len(failed_results)) + ' ' + + str([f.name for f in failed_results])) + logging.info('Remaining: ' + str(len(all_tests - executed_names)) + ' ' + + str(all_tests - executed_names)) + logging.info('*' * 80) + if executed_names == all_tests: + break + self.test_results = TestResults.FromOkAndFailed(list(executed_results - + failed_results), + list(failed_results)) + + def _RunTestsForSuiteInternal(self): + """Runs all tests (in rebaseline mode, run each test in isolation). + + Returns: + A TestResults object. + """ + if self.test_package.rebaseline: + self.RebaselineTests() + else: + if not self._gtest_filter: + self._gtest_filter = ('-' + ':'.join(self.GetDisabledTests()) + ':' + + ':'.join(['*.' + x + '*' for x in + self.test_package.GetDisabledPrefixes()])) + self.RunTestsWithFilter() + + def SetUp(self): + """Sets up necessary test enviroment for the test suite.""" + super(SingleTestRunner, self).SetUp() + if self.test_package.performance_test: + if run_tests_helper.IsRunningAsBuildbot(): + self.adb.SetJavaAssertsEnabled(enable=False) + self.adb.Reboot(full_reboot=False) + self.adb.SetupPerformanceTest() + if self.dump_debug_info: + self.dump_debug_info.StartRecordingLog(True) + self.StripAndCopyFiles() + self.LaunchHelperToolsForTestSuite() + self.test_package.tool.SetupEnvironment() + + def TearDown(self): + """Cleans up the test enviroment for the test suite.""" + super(SingleTestRunner, self).TearDown() + self.test_package.tool.CleanUpEnvironment() + if self.test_package.cleanup_test_files: + self.adb.RemovePushedFiles() + if self.dump_debug_info: + self.dump_debug_info.StopRecordingLog() + if self.test_package.performance_test: + self.adb.TearDownPerformanceTest() + + def RunTests(self): + """Runs the tests and cleans up the files once finished. + + Returns: + A TestResults object. + """ + self.SetUp() + try: + self._RunTestsForSuiteInternal() + finally: + self.TearDown() + return self.test_results diff --git a/build/android/test_package.py b/build/android/test_package.py new file mode 100644 index 0000000..dc35a0f --- /dev/null +++ b/build/android/test_package.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium 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 +import re +import os +import pexpect + +from perf_tests_helper import PrintPerfResult +from test_result import BaseTestResult, TestResults +from valgrind_tools import CreateTool + + +# TODO(bulach): TestPackage, TestPackageExecutable and +# TestPackageApk are a work in progress related to making the native tests +# run as a NDK-app from an APK rather than a stand-alone executable. +class TestPackage(object): + """A helper base class for both APK and stand-alone executables. + + Args: + adb: ADB interface the tests are using. + device: Device to run the tests. + test_suite: A specific test suite to run, empty to run all. + timeout: Timeout for each test. + rebaseline: Whether or not to run tests in isolation and update the filter. + performance_test: Whether or not performance test(s). + cleanup_test_files: Whether or not to cleanup test files on device. + tool: Name of the Valgrind tool. + dump_debug_info: A debug_info object. + """ + + def __init__(self, adb, device, test_suite, timeout, rebaseline, + performance_test, cleanup_test_files, tool, dump_debug_info): + self.adb = adb + self.device = device + self.test_suite = os.path.splitext(test_suite)[0] + self.test_suite_basename = os.path.basename(self.test_suite) + self.test_suite_dirname = os.path.dirname(self.test_suite) + self.rebaseline = rebaseline + self._performance_test = performance_test + self.cleanup_test_files = cleanup_test_files + self.tool = CreateTool(tool, self.adb) + if timeout == 0: + if self.test_suite_basename == 'page_cycler_tests': + timeout = 900 + else: + timeout = 60 + self.timeout = timeout * self.tool.GetTimeoutScale() + self.dump_debug_info = dump_debug_info + + def _BeginGetIOStats(self): + """Gets I/O statistics before running test. + + Return: + Tuple of (I/O stats object, flag of ready to continue). When encountering + error, ready-to-continue flag is False, True otherwise. The I/O stats + object may be None if the test is not performance test. + """ + initial_io_stats = None + # Try to get the disk I/O statistics for all performance tests. + if self._performance_test and not self.rebaseline: + initial_io_stats = self.adb.GetIoStats() + # Get rid of the noise introduced by launching Chrome for page cycler. + if self.test_suite_basename == 'page_cycler_tests': + try: + chrome_launch_done_re = re.compile( + re.escape('Finish waiting for browser launch!')) + self.adb.WaitForLogMatch(chrome_launch_done_re) + initial_io_stats = self.adb.GetIoStats() + except pexpect.TIMEOUT: + logging.error('Test terminated because Chrome launcher has no' + 'response after 120 second.') + return (None, False) + finally: + if self.dump_debug_info: + self.dump_debug_info.TakeScreenshot('_Launch_Chrome_') + return (initial_io_stats, True) + + def _EndGetIOStats(self, initial_io_stats): + """Gets I/O statistics after running test and calcuate the I/O delta. + + Args: + initial_io_stats: I/O stats object got from _BeginGetIOStats. + + Return: + String for formated diso I/O statistics. + """ + disk_io = '' + if self._performance_test and initial_io_stats: + final_io_stats = self.adb.GetIoStats() + for stat in final_io_stats: + disk_io += '\n' + PrintPerfResult(stat, stat, + [final_io_stats[stat] - + initial_io_stats[stat]], + stat.split('_')[1], True, False) + logging.info(disk_io) + return disk_io + + def GetDisabledPrefixes(self): + return ['DISABLED_', 'FLAKY_', 'FAILS_'] + + def _ParseGTestListTests(self, all_tests): + ret = [] + current = '' + disabled_prefixes = self.GetDisabledPrefixes() + for test in all_tests: + if not test: + continue + if test[0] != ' ': + current = test + continue + if 'YOU HAVE' in test: + break + test_name = test[2:] + if not any([test_name.startswith(x) for x in disabled_prefixes]): + ret += [current + test_name] + return ret + + def _WatchTestOutput(self, p): + """Watches the test output. + Args: + p: the process generating output as created by pexpect.spawn. + """ + ok_tests = [] + failed_tests = [] + re_run = re.compile('\[ RUN \] ?(.*)\r\n') + re_fail = re.compile('\[ FAILED \] ?(.*)\r\n') + re_ok = re.compile('\[ OK \] ?(.*)\r\n') + (io_stats_before, ready_to_continue) = self._BeginGetIOStats() + while ready_to_continue: + found = p.expect([re_run, pexpect.EOF], timeout=self.timeout) + if found == 1: # matched pexpect.EOF + break + if self.dump_debug_info: + self.dump_debug_info.TakeScreenshot('_Test_Start_Run_') + full_test_name = p.match.group(1) + found = p.expect([re_ok, re_fail, pexpect.EOF, pexpect.TIMEOUT], + timeout=self.timeout) + if found == 0: # re_ok + ok_tests += [BaseTestResult(full_test_name.replace('\r', ''), + p.before)] + continue + failed_tests += [BaseTestResult(full_test_name.replace('\r', ''), + p.before)] + if found >= 2: + # The test crashed / bailed out (i.e., didn't print OK or FAIL). + if found == 3: # pexpect.TIMEOUT + logging.error('Test terminated after %d second timeout.', + self.timeout) + break + p.close() + if not self.rebaseline and ready_to_continue: + ok_tests += self._EndGetIOStats(io_stats_before) + ret_code = self._GetGTestReturnCode() + if ret_code: + failed_tests += [BaseTestResult('gtest exit code: %d' % ret_code, + 'pexpect.before: %s' + '\npexpect.after: %s' + % (p.before, + p.after))] + return TestResults.FromOkAndFailed(ok_tests, failed_tests) diff --git a/build/android/test_package_executable.py b/build/android/test_package_executable.py new file mode 100644 index 0000000..6390735 --- /dev/null +++ b/build/android/test_package_executable.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium 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 +import os +import pexpect +import shutil +import sys +import tempfile + +import cmd_helper +from test_package import TestPackage + + +class TestPackageExecutable(TestPackage): + """A helper class for running stand-alone executables.""" + + _TEST_RUNNER_RET_VAL_FILE = '/data/local/tmp/gtest_retval' + + def __init__(self, adb, device, test_suite, timeout, rebaseline, + performance_test, cleanup_test_files, tool, dump_debug_info, + symbols_dir=None): + """ + Args: + adb: ADB interface the tests are using. + device: Device to run the tests. + test_suite: A specific test suite to run, empty to run all. + timeout: Timeout for each test. + rebaseline: Whether or not to run tests in isolation and update the + filter. + performance_test: Whether or not performance test(s). + cleanup_test_files: Whether or not to cleanup test files on device. + tool: Name of the Valgrind tool. + dump_debug_info: A debug_info object. + symbols_dir: Directory to put the stripped binaries. + """ + TestPackage.__init__(self, adb, device, test_suite, timeout, + rebaseline, performance_test, cleanup_test_files, + tool, dump_debug_info) + self.symbols_dir = symbols_dir + + def _GetGTestReturnCode(self): + ret = None + ret_code_file = tempfile.NamedTemporaryFile() + try: + if not self.adb.Adb().Pull( + TestPackageExecutable._TEST_RUNNER_RET_VAL_FILE, ret_code_file.name): + logging.critical('Unable to pull gtest ret val file %s', + ret_code_file.name) + raise ValueError + ret_code = file(ret_code_file.name).read() + ret = int(ret_code) + except ValueError: + logging.critical('Error reading gtest ret val file %s [%s]', + ret_code_file.name, ret_code) + ret = 1 + return ret + + def _AddNativeCoverageExports(self): + # export GCOV_PREFIX set the path for native coverage results + # export GCOV_PREFIX_STRIP indicates how many initial directory + # names to strip off the hardwired absolute paths. + # This value is calculated in buildbot.sh and + # depends on where the tree is built. + # Ex: /usr/local/google/code/chrome will become + # /code/chrome if GCOV_PREFIX_STRIP=3 + try: + depth = os.environ['NATIVE_COVERAGE_DEPTH_STRIP'] + except KeyError: + logging.info('NATIVE_COVERAGE_DEPTH_STRIP is not defined: ' + 'No native coverage.') + return '' + export_string = 'export GCOV_PREFIX="/data/local/gcov"\n' + export_string += 'export GCOV_PREFIX_STRIP=%s\n' % depth + return export_string + + def GetAllTests(self): + """Returns a list of all tests available in the test suite.""" + all_tests = self.adb.RunShellCommand( + '/data/local/%s --gtest_list_tests' % self.test_suite_basename) + return self._ParseGTestListTests(all_tests) + + def CreateTestRunnerScript(self, gtest_filter, test_arguments): + """Creates a test runner script and pushes to the device. + + Args: + gtest_filter: A gtest_filter flag. + test_arguments: Additional arguments to pass to the test binary. + """ + tool_wrapper = self.tool.GetTestWrapper() + sh_script_file = tempfile.NamedTemporaryFile() + # We need to capture the exit status from the script since adb shell won't + # propagate to us. + sh_script_file.write('cd /data/local\n' + '%s' + '%s /data/local/%s --gtest_filter=%s %s\n' + 'echo $? > %s' % + (self._AddNativeCoverageExports(), + tool_wrapper, self.test_suite_basename, + gtest_filter, test_arguments, + TestPackageExecutable._TEST_RUNNER_RET_VAL_FILE)) + sh_script_file.flush() + cmd_helper.RunCmd(['chmod', '+x', sh_script_file.name]) + self.adb.PushIfNeeded(sh_script_file.name, + '/data/local/chrome_test_runner.sh') + + def RunTestsAndListResults(self): + """Runs all the tests and checks for failures. + + Returns: + A TestResults object. + """ + args = ['adb', '-s', self.device, 'shell', 'sh', + '/data/local/chrome_test_runner.sh'] + logging.info(args) + p = pexpect.spawn(args[0], args[1:], logfile=sys.stdout) + return self._WatchTestOutput(p) + + def StripAndCopyExecutable(self): + """Strips and copies the executable to the device.""" + if self.tool.NeedsDebugInfo(): + target_name = self.test_suite + elif self.test_suite_basename == 'webkit_unit_tests': + # webkit_unit_tests has been stripped in build step. + target_name = self.test_suite + else: + target_name = self.test_suite + '_' + self.device + '_stripped' + should_strip = True + if os.path.isfile(target_name): + logging.info('Found target file %s' % target_name) + target_mtime = os.stat(target_name).st_mtime + source_mtime = os.stat(self.test_suite).st_mtime + if target_mtime > source_mtime: + logging.info('Target mtime (%d) is newer than source (%d), assuming ' + 'no change.' % (target_mtime, source_mtime)) + should_strip = False + + if should_strip: + logging.info('Did not find up-to-date stripped binary. Generating a ' + 'new one (%s).' % target_name) + # Whenever we generate a stripped binary, copy to the symbols dir. If we + # aren't stripping a new binary, assume it's there. + if self.symbols_dir: + if not os.path.exists(self.symbols_dir): + os.makedirs(self.symbols_dir) + shutil.copy(self.test_suite, self.symbols_dir) + strip = os.environ['STRIP'] + cmd_helper.RunCmd([strip, self.test_suite, '-o', target_name]) + test_binary = '/data/local/' + self.test_suite_basename + self.adb.PushIfNeeded(target_name, test_binary) diff --git a/build/android/test_result.py b/build/android/test_result.py new file mode 100644 index 0000000..e5b0dc1 --- /dev/null +++ b/build/android/test_result.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# Copyright (c) 2011 The Chromium 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 + + +# Language values match constants in Sponge protocol buffer (sponge.proto). +JAVA = 5 +PYTHON = 7 + + +class BaseTestResult(object): + """A single result from a unit test.""" + + def __init__(self, name, log): + self.name = name + self.log = log + + +class SingleTestResult(BaseTestResult): + """Result information for a single test. + + Args: + full_name: Full name of the test. + start_date: Date in milliseconds when the test began running. + dur: Duration of the test run in milliseconds. + lang: Language of the test (JAVA or PYTHON). + log: An optional string listing any errors. + error: A tuple of a short error message and a longer version used by Sponge + if test resulted in a fail or error. An empty tuple implies a pass. + """ + + def __init__(self, full_name, start_date, dur, lang, log='', error=()): + BaseTestResult.__init__(self, full_name, log) + name_pieces = full_name.rsplit('#') + if len(name_pieces) > 0: + self.test_name = name_pieces[1] + self.class_name = name_pieces[0] + else: + self.class_name = full_name + self.test_name = full_name + self.start_date = start_date + self.dur = dur + self.error = error + self.lang = lang + + +class TestResults(object): + """Results of a test run.""" + + def __init__(self): + self.ok = [] + self.failed = [] + self.crashed = [] + self.unknown = [] + self.disabled = [] + self.unexpected_pass = [] + + @staticmethod + def FromOkAndFailed(ok, failed): + ret = TestResults() + ret.ok = ok + ret.failed = failed + return ret + + @staticmethod + def FromTestResults(results): + """Combines a list of results in a single TestResults object.""" + ret = TestResults() + for t in results: + ret.ok += t.ok + ret.failed += t.failed + ret.crashed += t.crashed + ret.unknown += t.unknown + ret.disabled += t.disabled + ret.unexpected_pass += t.unexpected_pass + return ret + + def _Log(self, sorted_list): + for t in sorted_list: + logging.critical(t.name) + if t.log: + logging.critical(t.log) + + def GetAllBroken(self): + """Returns the all broken tests including failed, crashed, unknown.""" + return self.failed + self.crashed + self.unknown + + def LogFull(self): + """Output all broken tests or 'passed' if none broken""" + logging.critical('*' * 80) + logging.critical('Final result') + if self.failed: + logging.critical('Failed:') + self._Log(sorted(self.failed)) + if self.crashed: + logging.critical('Crashed:') + self._Log(sorted(self.crashed)) + if self.unknown: + logging.critical('Unknown:') + self._Log(sorted(self.unknown)) + if not self.GetAllBroken(): + logging.critical('Passed') + logging.critical('*' * 80) |