diff options
author | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2015-08-14 11:38:45 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2015-08-14 17:16:47 +0000 |
commit | 3a97ca8dd9b96b599ae2d33e40df0dd2f7ea5859 (patch) | |
tree | 43cc572ba067417c7341db81f71ae7cc6e0fcc3e /chromium/mojo/tools | |
parent | f61ab1ac7f855cd281809255c0aedbb1895e1823 (diff) | |
download | qtwebengine-chromium-3a97ca8dd9b96b599ae2d33e40df0dd2f7ea5859.tar.gz |
BASELINE: Update chromium to 45.0.2454.40
Change-Id: Id2121d9f11a8fc633677236c65a3e41feef589e4
Reviewed-by: Andras Becsi <andras.becsi@theqtcompany.com>
Diffstat (limited to 'chromium/mojo/tools')
-rwxr-xr-x | chromium/mojo/tools/android_mojo_shell.py | 29 | ||||
-rwxr-xr-x | chromium/mojo/tools/apptest_runner.py | 180 | ||||
-rw-r--r-- | chromium/mojo/tools/data/apptests | 61 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/android.py | 537 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/config.py | 125 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/dart_apptest.py | 38 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/gn.py | 141 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/gtest.py | 265 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/log.py | 29 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/paths.py | 73 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/print_process_error.py | 22 | ||||
-rw-r--r-- | chromium/mojo/tools/mopy/test_util.py | 95 | ||||
-rwxr-xr-x | chromium/mojo/tools/rev_sdk.py | 15 |
13 files changed, 669 insertions, 941 deletions
diff --git a/chromium/mojo/tools/android_mojo_shell.py b/chromium/mojo/tools/android_mojo_shell.py index 73541d2bd6f..032557f5795 100755 --- a/chromium/mojo/tools/android_mojo_shell.py +++ b/chromium/mojo/tools/android_mojo_shell.py @@ -9,7 +9,6 @@ import sys from mopy.android import AndroidShell from mopy.config import Config -from mopy.paths import Paths USAGE = ("android_mojo_shell.py [<shell-and-app-args>] [<mojo-app>]") @@ -24,26 +23,26 @@ def main(): debug_group.add_argument('--release', help='Release build', default=False, dest='debug', action='store_false') parser.add_argument('--target-cpu', help='CPU architecture to run for.', - choices=['x64', 'x86', 'arm']) + choices=['x64', 'x86', 'arm'], default='arm') parser.add_argument('--origin', help='Origin for mojo: URLs.', default='localhost') - parser.add_argument('--target-device', help='Device to run on.') - launcher_args, args = parser.parse_known_args() + parser.add_argument('--device', help='Serial number of the target device.') + parser.add_argument("--verbose", default=False, action='store_true') + runner_args, args = parser.parse_known_args() + + logger = logging.getLogger() + logging.basicConfig(stream=sys.stdout, format="%(levelname)s:%(message)s") + logger.setLevel(logging.DEBUG if runner_args.verbose else logging.WARNING) + logger.debug("Initialized logging: level=%s" % logger.level) config = Config(target_os=Config.OS_ANDROID, - target_cpu=launcher_args.target_cpu, - is_debug=launcher_args.debug, + target_cpu=runner_args.target_cpu, + is_debug=runner_args.debug, apk_name="MojoRunner.apk") - paths = Paths(config) - shell = AndroidShell(paths.target_mojo_shell_path, paths.build_dir, - paths.adb_path, launcher_args.target_device) - - extra_shell_args = shell.PrepareShellRun(launcher_args.origin) - args.extend(extra_shell_args) - - shell.CleanLogs() + shell = AndroidShell(config) + shell.InitShell(runner_args.origin, runner_args.device) p = shell.ShowLogs() - shell.StartShell(args, sys.stdout, p.terminate) + shell.StartActivity('MojoShellActivity', args, sys.stdout, p.terminate) return 0 diff --git a/chromium/mojo/tools/apptest_runner.py b/chromium/mojo/tools/apptest_runner.py index 21f25fda82b..6e0d2a3885c 100755 --- a/chromium/mojo/tools/apptest_runner.py +++ b/chromium/mojo/tools/apptest_runner.py @@ -6,84 +6,140 @@ """A test runner for gtest application tests.""" import argparse +import json import logging +import os import sys +import time -from mopy import dart_apptest from mopy import gtest -from mopy.android import AndroidShell from mopy.config import Config -from mopy.gn import ConfigForGNArgs, ParseGNConfig -from mopy.log import InitLogging -from mopy.paths import Paths - - -_logger = logging.getLogger() def main(): - parser = argparse.ArgumentParser(description="A test runner for application " - "tests.") - - parser.add_argument("--verbose", help="be verbose (multiple times for more)", - default=0, dest="verbose_count", action="count") - parser.add_argument("test_list_file", type=file, - help="a file listing apptests to run") - parser.add_argument("build_dir", type=str, - help="the build output directory") + parser = argparse.ArgumentParser(description="An application test runner.") + parser.add_argument("build_dir", type=str, help="The build output directory.") + parser.add_argument("--verbose", default=False, action='store_true', + help="Print additional logging information.") + parser.add_argument('--repeat-count', default=1, metavar='INT', + action='store', type=int, + help="The number of times to repeat the set of tests.") + parser.add_argument('--write-full-results-to', metavar='FILENAME', + help='The path to write the JSON list of full results.') + parser.add_argument("--test-list-file", metavar='FILENAME', type=file, + default=os.path.abspath(os.path.join(__file__, os.pardir, + "data", "apptests")), + help="The file listing apptests to run.") args = parser.parse_args() - InitLogging(args.verbose_count) - config = ConfigForGNArgs(ParseGNConfig(args.build_dir)) + gtest.set_color() + logger = logging.getLogger() + logging.basicConfig(stream=sys.stdout, format="%(levelname)s:%(message)s") + logger.setLevel(logging.DEBUG if args.verbose else logging.WARNING) + logger.debug("Initialized logging: level=%s" % logger.level) - _logger.debug("Test list file: %s", args.test_list_file) + logger.debug("Test list file: %s", args.test_list_file) + config = Config(args.build_dir) execution_globals = {"config": config} exec args.test_list_file in execution_globals test_list = execution_globals["tests"] - _logger.debug("Test list: %s" % test_list) + logger.debug("Test list: %s" % test_list) - extra_args = [] + shell = None if config.target_os == Config.OS_ANDROID: - paths = Paths(config) - shell = AndroidShell(paths.target_mojo_shell_path, paths.build_dir, - paths.adb_path) - extra_args.extend(shell.PrepareShellRun('localhost')) - else: - shell = None - - gtest.set_color() - - exit_code = 0 - for test_dict in test_list: - test = test_dict["test"] - test_name = test_dict.get("name", test) - test_type = test_dict.get("type", "gtest") - test_args = test_dict.get("test-args", []) - shell_args = test_dict.get("shell-args", []) + extra_args - - _logger.info("Will start: %s" % test_name) - print "Running %s...." % test_name, - sys.stdout.flush() - - if test_type == "dart": - apptest_result = dart_apptest.run_test(config, shell, test_dict, - shell_args, {test: test_args}) - elif test_type == "gtest": - apptest_result = gtest.run_fixtures(config, shell, test_dict, - test, False, - test_args, shell_args) - elif test_type == "gtest_isolated": - apptest_result = gtest.run_fixtures(config, shell, test_dict, - test, True, test_args, shell_args) - else: - apptest_result = "Invalid test type in %r" % test_dict - - if apptest_result != "Succeeded": - exit_code = 1 - print apptest_result - _logger.info("Completed: %s" % test_name) - - return exit_code + from mopy.android import AndroidShell + shell = AndroidShell(config) + result = shell.InitShell() + if result != 0: + return result + + tests = [] + failed = [] + failed_suites = 0 + for _ in range(args.repeat_count): + for test_dict in test_list: + test = test_dict["test"] + test_name = test_dict.get("name", test) + test_type = test_dict.get("type", "gtest") + test_args = test_dict.get("args", []) + + print "Running %s...%s" % (test_name, ("\n" if args.verbose else "")), + sys.stdout.flush() + + assert test_type in ("gtest", "gtest_isolated") + isolate = test_type == "gtest_isolated" + (test, fail) = gtest.run_apptest(config, shell, test_args, test, isolate) + tests.extend(test) + failed.extend(fail) + result = test and not fail + print "[ PASSED ]" if result else "[ FAILED ]", + print test_name if args.verbose or not result else "" + # Abort when 3 apptest suites, or a tenth of all, have failed. + # base::TestLauncher does this for timeouts and unknown results. + failed_suites += 0 if result else 1 + if failed_suites >= max(3, len(test_list) / 10): + print "Too many failing suites (%d), exiting now." % failed_suites + failed.append("Test runner aborted for excessive failures.") + break; + + if failed: + break; + + print "[==========] %d tests ran." % len(tests) + print "[ PASSED ] %d tests." % (len(tests) - len(failed)) + if failed: + print "[ FAILED ] %d tests, listed below:" % len(failed) + for failure in failed: + print "[ FAILED ] %s" % failure + + if args.write_full_results_to: + _WriteJSONResults(tests, failed, args.write_full_results_to) + + return 1 if failed else 0 + + +def _WriteJSONResults(tests, failed, write_full_results_to): + """Write the apptest results in the Chromium JSON test results format. + See <http://www.chromium.org/developers/the-json-test-results-format> + TODO(msw): Use Chromium and TYP testing infrastructure. + TODO(msw): Use GTest Suite.Fixture names, not the apptest names. + Adapted from chrome/test/mini_installer/test_installer.py + """ + results = { + 'interrupted': False, + 'path_delimiter': '.', + 'version': 3, + 'seconds_since_epoch': time.time(), + 'num_failures_by_type': { + 'FAIL': len(failed), + 'PASS': len(tests) - len(failed), + }, + 'tests': {} + } + + for test in tests: + value = { + 'expected': 'PASS', + 'actual': 'FAIL' if test in failed else 'PASS', + 'is_unexpected': True if test in failed else False, + } + _AddPathToTrie(results['tests'], test, value) + + with open(write_full_results_to, 'w') as fp: + json.dump(results, fp, indent=2) + fp.write('\n') + + return results + + +def _AddPathToTrie(trie, path, value): + if '.' not in path: + trie[path] = value + return + directory, rest = path.split('.', 1) + if directory not in trie: + trie[directory] = {} + _AddPathToTrie(trie[directory], rest, value) if __name__ == '__main__': diff --git a/chromium/mojo/tools/data/apptests b/chromium/mojo/tools/data/apptests index 54a26adbc3c..140ee6a60aa 100644 --- a/chromium/mojo/tools/data/apptests +++ b/chromium/mojo/tools/data/apptests @@ -11,15 +11,12 @@ # # Optional display name (otherwise the entry for "test" above is used). # "name": "mojo:test_app_url (more details)", # # Optional test type. Valid values: -# # * "gtest" (default) -# # * "gtest_isolated": like "gtest", but run with fixture isolation, -# # i.e., each test in a fresh mojo_shell) -# # * "dart". +# # * "gtest": (default) +# # * "gtest_isolated": like "gtest", but run with fixture isolation. +# # i.e., each test in a fresh mojo_shell # "type": "gtest", -# # Optional arguments for the apptest. -# "test-args": ["--an_arg", "another_arg"], -# # Optional arguments for the shell. -# "shell-args": ["--some-flag-for-the-shell", "--another-flag"], +# # Optional arguments for the shell or test. +# "args": ["--some-flag-for-the-shell", "--some-flag-for-the-test"], # } # # TODO(vtl|msw): Add a way of specifying data dependencies. @@ -27,32 +24,64 @@ tests = [ { "test": "mojo:clipboard_apptests", + "type": "gtest_isolated", }, { "test": "mojo:network_service_apptests", + "type": "gtest_isolated", }, - # TODO(msw|jam): Fix and enable the shell_apptests: http://crbug.com/479316 + # TODO(msw|jam): Fix and enable the runner_apptests: http://crbug.com/479316 #{ - # "test": "mojo:shell_apptests", + # "test": "mojo:runner_apptests", + # "type": "gtest_isolated", #}, + { + "test": "mojo:view_manager_apptests", + "type": "gtest_isolated", + "args": ["--use-headless-config"] + }, ] # TODO(msw): Get these tests passing on Android too. http://crbug.com/486220 if config.target_os != config.OS_ANDROID: tests += [ { + "test": "mojo:filesystem_apptests", + "type": "gtest_isolated", + }, + { "test": "mojo:html_viewer_apptests", - "shell-args": ["--is-headless"], + "type": "gtest_isolated", + "args": ["--use-test-config"] + }, + { + "test": "mojo:html_viewer_apptests", + "type": "gtest_isolated", + "args": ["--use-test-config", "--oopifs", + "--enable-html-viewer-test-interface"] }, { - "test": "mojo:view_manager_apptests", + "test": "mojo:mandoline_browser_apptests", + "type": "gtest_isolated", + "args": ["--use-headless-config"] + }, + { + "test": "mojo:mandoline_frame_apptests", + "type": "gtest_isolated", + "args": ["--use-headless-config"] + }, + # TODO(xhwang): Fix and enable mojo:media_pipeline_integration_apptests. + # http://crbug.com/501417 + { + "test": "mojo:media_apptests", "type": "gtest_isolated", - "shell-args": [ - "--use-headless-config", - "--url-mappings=mojo:window_manager=mojo:test_window_manager" - ] }, { "test": "mojo:resource_provider_apptests", + "type": "gtest_isolated", + }, + { + "test": "mojo:sql_apptests", + "type": "gtest_isolated", }, ] diff --git a/chromium/mojo/tools/mopy/android.py b/chromium/mojo/tools/mopy/android.py index 2354c8e5f91..ec275db37f0 100644 --- a/chromium/mojo/tools/mopy/android.py +++ b/chromium/mojo/tools/mopy/android.py @@ -3,24 +3,26 @@ # found in the LICENSE file. import atexit -import datetime -import email.utils -import hashlib import itertools -import json import logging -import math import os -import os.path -import random +import signal import subprocess import sys import threading import time import urlparse -import SimpleHTTPServer -import SocketServer +from .paths import Paths + +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, + os.pardir, 'build', 'android')) +from pylib import constants +from pylib.base import base_test_runner +from pylib.device import device_errors +from pylib.device import device_utils +from pylib.utils import base_error +from pylib.utils import apk_helper # Tags used by the mojo shell application logs. @@ -33,274 +35,93 @@ LOGCAT_TAGS = [ 'chromium', ] -MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell' - MAPPING_PREFIX = '--map-origin=' -ZERO = datetime.timedelta(0) - -class UTC_TZINFO(datetime.tzinfo): - """UTC time zone representation.""" - - def utcoffset(self, _): - return ZERO - - def tzname(self, _): - return "UTC" - - def dst(self, _): - return ZERO - -UTC = UTC_TZINFO() - -_logger = logging.getLogger() - - -class _SilentTCPServer(SocketServer.TCPServer): - """ - A TCPServer that won't display any error, unless debugging is enabled. This is - useful because the client might stop while it is fetching an URL, which causes - spurious error messages. - """ - def handle_error(self, request, client_address): - """ - Override the base class method to have conditional logging. - """ - if logging.getLogger().isEnabledFor(logging.DEBUG): - SocketServer.TCPServer.handle_error(self, request, client_address) - - -def _GetHandlerClassForPath(base_path): - class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - """ - Handler for SocketServer.TCPServer that will serve the files from - |base_path| directory over http. - """ - - def __init__(self, *args, **kwargs): - self.etag = None - SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) - - def get_etag(self): - if self.etag: - return self.etag - - path = self.translate_path(self.path) - if not os.path.isfile(path): - return None - - sha256 = hashlib.sha256() - BLOCKSIZE = 65536 - with open(path, 'rb') as hashed: - buf = hashed.read(BLOCKSIZE) - while len(buf) > 0: - sha256.update(buf) - buf = hashed.read(BLOCKSIZE) - self.etag = '"%s"' % sha256.hexdigest() - return self.etag - - def send_head(self): - # Always close the connection after each request, as the keep alive - # support from SimpleHTTPServer doesn't like when the client requests to - # close the connection before downloading the full response content. - # pylint: disable=W0201 - self.close_connection = 1 - - path = self.translate_path(self.path) - if os.path.isfile(path): - # Handle If-None-Match - etag = self.get_etag() - if ('If-None-Match' in self.headers and - etag == self.headers['If-None-Match']): - self.send_response(304) - return None - - # Handle If-Modified-Since - if ('If-None-Match' not in self.headers and - 'If-Modified-Since' in self.headers): - last_modified = datetime.datetime.fromtimestamp( - math.floor(os.stat(path).st_mtime), tz=UTC) - ims = datetime.datetime( - *email.utils.parsedate(self.headers['If-Modified-Since'])[:6], - tzinfo=UTC) - if last_modified <= ims: - self.send_response(304) - return None - - return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) - - def end_headers(self): - path = self.translate_path(self.path) - - if os.path.isfile(path): - etag = self.get_etag() - if etag: - self.send_header('ETag', etag) - self.send_header('Cache-Control', 'must-revalidate') - - return SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self) - - def translate_path(self, path): - path_from_current = ( - SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path)) - return os.path.join(base_path, os.path.relpath(path_from_current)) - - def log_message(self, *_): - """ - Override the base class method to disable logging. - """ - pass - - RequestHandler.protocol_version = 'HTTP/1.1' - return RequestHandler - - -def _IsMapOrigin(arg): - """Returns whether arg is a --map-origin argument.""" - return arg.startswith(MAPPING_PREFIX) - - -def _Split(l, pred): - positive = [] - negative = [] - for v in l: - if pred(v): - positive.append(v) - else: - negative.append(v) - return (positive, negative) - def _ExitIfNeeded(process): - """ - Exits |process| if it is still alive. - """ + """Exits |process| if it is still alive.""" if process.poll() is None: process.kill() class AndroidShell(object): - """ Allows to set up and run a given mojo shell binary on an Android device. - - Args: - shell_apk_path: path to the shell Android binary - local_dir: directory where locally build Mojo apps will be served, optional - adb_path: path to adb, optional if adb is in PATH - target_device: device to run on, if multiple devices are connected """ - def __init__( - self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None, - target_package=MOJO_SHELL_PACKAGE_NAME): - self.shell_apk_path = shell_apk_path - self.adb_path = adb_path - self.local_dir = local_dir - self.target_device = target_device - self.target_package = target_package - + Used to set up and run a given mojo shell binary on an Android device. + |config| is the mopy.config.Config for the build. + """ + def __init__(self, config): + self.adb_path = constants.GetAdbPath() + self.config = config + self.paths = Paths(config) + self.device = None + self.shell_args = [] + self.target_package = apk_helper.GetPackageName(self.paths.apk_path) + self.temp_gdb_dir = None + # This is used by decive_utils.Install to check if the apk needs updating. + constants.SetOutputDirectory(self.paths.build_dir) + + # TODO(msw): Use pylib's adb_wrapper and device_utils instead. def _CreateADBCommand(self, args): - adb_command = [self.adb_path] - if self.target_device: - adb_command.extend(['-s', self.target_device]) + adb_command = [self.adb_path, '-s', self.device.adb.GetDeviceSerial()] adb_command.extend(args) + logging.getLogger().debug("Command: %s", " ".join(adb_command)) return adb_command - def _ReadFifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): + def _ReadFifo(self, path, pipe, on_fifo_closed, max_attempts=5): """ - Reads |fifo_path| on the device and write the contents to |pipe|. Calls - |on_fifo_closed| when the fifo is closed. This method will try to find the - path up to |max_attempts|, waiting 1 second between each attempt. If it - cannot find |fifo_path|, a exception will be raised. + Reads the fifo at |path| on the device and write the contents to |pipe|. + Calls |on_fifo_closed| when the fifo is closed. This method will try to find + the path up to |max_attempts|, waiting 1 second between each attempt. If it + cannot find |path|, a exception will be raised. """ - fifo_command = self._CreateADBCommand( - ['shell', 'test -e "%s"; echo $?' % fifo_path]) - def Run(): def _WaitForFifo(): for _ in xrange(max_attempts): - if subprocess.check_output(fifo_command)[0] == '0': + if self.device.FileExists(path): return time.sleep(1) - if on_fifo_closed: - on_fifo_closed() - raise Exception("Unable to find fifo.") + on_fifo_closed() + raise Exception("Unable to find fifo: %s" % path) _WaitForFifo() stdout_cat = subprocess.Popen(self._CreateADBCommand([ - 'shell', - 'cat', - fifo_path]), + 'shell', + 'cat', + path]), stdout=pipe) atexit.register(_ExitIfNeeded, stdout_cat) stdout_cat.wait() - if on_fifo_closed: - on_fifo_closed() + on_fifo_closed() thread = threading.Thread(target=Run, name="StdoutRedirector") thread.start() - def _MapPort(self, device_port, host_port): + def _StartHttpServerForDirectory(self, path): + test_server_helper = base_test_runner.BaseTestRunner(self.device, None) + ports = test_server_helper.LaunchTestHttpServer(path) + atexit.register(test_server_helper.ShutdownHelperToolsForTestSuite) + print 'Hosting %s at http://127.0.0.1:%d' % (path, ports[1]) + return 'http://127.0.0.1:%d/' % ports[0] + + def _StartHttpServerForOriginMapping(self, mapping): """ - Maps the device port to the host port. If |device_port| is 0, a random - available port is chosen. Returns the device port. + If |mapping| points at a local file starts an http server to serve files + from the directory and returns the new mapping. This is intended to be + called for every --map-origin value. """ - def _FindAvailablePortOnDevice(): - opened = subprocess.check_output( - self._CreateADBCommand(['shell', 'netstat'])) - opened = [int(x.strip().split()[3].split(':')[1]) - for x in opened if x.startswith(' tcp')] - while True: - port = random.randint(4096, 16384) - if port not in opened: - return port - if device_port == 0: - device_port = _FindAvailablePortOnDevice() - subprocess.Popen(self._CreateADBCommand([ - "reverse", - "tcp:%d" % device_port, - "tcp:%d" % host_port])).wait() - - unmap_command = self._CreateADBCommand(["reverse", "--remove", - "tcp:%d" % device_port]) - - def _UnmapPort(): - subprocess.Popen(unmap_command) - atexit.register(_UnmapPort) - return device_port - - def _StartHttpServerForDirectory(self, path, port=0): - """Starts an http server serving files from |path|. Returns the local - url.""" - assert path - httpd = _SilentTCPServer(('127.0.0.1', 0), _GetHandlerClassForPath(path)) - atexit.register(httpd.shutdown) - - http_thread = threading.Thread(target=httpd.serve_forever) - http_thread.daemon = True - http_thread.start() - - print 'Hosting %s at http://127.0.0.1:%d' % (path, httpd.server_address[1]) - return 'http://127.0.0.1:%d/' % self._MapPort(port, httpd.server_address[1]) - - def _StartHttpServerForOriginMapping(self, mapping, port): - """If |mapping| points at a local file starts an http server to serve files - from the directory and returns the new mapping. - - This is intended to be called for every --map-origin value.""" parts = mapping.split('=') if len(parts) != 2: return mapping dest = parts[1] - # If the destination is a url, don't map it. + # If the destination is a URL, don't map it. if urlparse.urlparse(dest)[0]: return mapping # Assume the destination is a local file. Start a local server that # redirects to it. - localUrl = self._StartHttpServerForDirectory(dest, port) - print 'started server at %s for %s' % (dest, localUrl) + localUrl = self._StartHttpServerForDirectory(dest) return parts[0] + '=' + localUrl def _StartHttpServerForOriginMappings(self, map_parameters): - """Calls _StartHttpServerForOriginMapping for every --map-origin - argument.""" + """Calls _StartHttpServerForOriginMapping for every --map-origin arg.""" if not map_parameters: return [] @@ -309,41 +130,123 @@ class AndroidShell(object): sorted(original_values) result = [] for value in original_values: - result.append(self._StartHttpServerForOriginMapping(value, 0)) + result.append(self._StartHttpServerForOriginMapping(value)) return [MAPPING_PREFIX + ','.join(result)] - def PrepareShellRun(self, origin=None): - """ Prepares for StartShell: runs adb as root and installs the apk. If the - origin specified is 'localhost', a local http server will be set up to serve - files from the build directory along with port forwarding. - - Returns arguments that should be appended to shell argument list.""" - if 'cannot run as root' in subprocess.check_output( - self._CreateADBCommand(['root'])): - raise Exception("Unable to run adb as root.") - subprocess.check_call( - self._CreateADBCommand(['install', '-r', self.shell_apk_path, '-i', - self.target_package])) - atexit.register(self.StopShell) - - extra_shell_args = [] + def InitShell(self, origin='localhost', device=None): + """ + Runs adb as root, starts an origin server, and installs the apk as needed. + |origin| is the origin for mojo: URLs; if its value is 'localhost', a local + http server will be set up to serve files from the build directory. + |device| is the target device to run on, if multiple devices are connected. + Returns 0 on success or a non-zero exit code on a terminal failure. + """ + try: + devices = device_utils.DeviceUtils.HealthyDevices() + if device: + self.device = next((d for d in devices if d == device), None) + if not self.device: + raise device_errors.DeviceUnreachableError(device) + elif devices: + self.device = devices[0] + else: + raise device_errors.NoDevicesError() + + logging.getLogger().debug("Using device: %s", self.device) + # Clean the logs on the device to avoid displaying prior activity. + subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) + self.device.EnableRoot() + self.device.Install(self.paths.apk_path) + except base_error.BaseError as e: + # Report "device not found" as infra failures. See http://crbug.com/493900 + print "Exception in AndroidShell.InitShell:\n%s" % str(e) + if e.is_infra_error or "error: device not found" in str(e): + return constants.INFRA_EXIT_CODE + return constants.ERROR_EXIT_CODE + if origin is 'localhost': - origin = self._StartHttpServerForDirectory(self.local_dir, 0) + origin = self._StartHttpServerForDirectory(self.paths.build_dir) if origin: - extra_shell_args.append("--origin=" + origin) - return extra_shell_args + self.shell_args.append("--origin=" + origin) + return 0 + + def _GetProcessId(self, process): + """Returns the process id of the process on the remote device.""" + while True: + line = process.stdout.readline() + pid_command = 'launcher waiting for GDB. pid: ' + index = line.find(pid_command) + if index != -1: + return line[index + len(pid_command):].strip() + return 0 + + def _GetLocalGdbPath(self): + """Returns the path to the android gdb.""" + if self.config.target_cpu == "arm": + return os.path.join(constants.ANDROID_NDK_ROOT, "toolchains", + "arm-linux-androideabi-4.9", "prebuilt", + "linux-x86_64", "bin", "arm-linux-androideabi-gdb") + elif self.config.target_cpu == "x86": + return os.path.join(constants.ANDROID_NDK_ROOT, "toolchains", + "x86-4.9", "prebuilt", "linux-x86_64", "bin", + "i686-linux-android-gdb") + elif self.config.target_cpu == "x64": + return os.path.join(constants.ANDROID_NDK_ROOT, "toolchains", + "x86_64-4.9", "prebuilt", "linux-x86_64", "bin", + "x86_64-linux-android-gdb") + else: + raise Exception("Unknown target_cpu: %s" % self.config.target_cpu) - def StartShell(self, - arguments, - stdout=None, - on_application_stop=None): + def _WaitForProcessIdAndStartGdb(self, process): """ - Starts the mojo shell, passing it the given arguments. - - The |arguments| list must contain the "--origin=" arg from PrepareShellRun. - If |stdout| is not None, it should be a valid argument for subprocess.Popen. + Waits until we see the process id from the remote device, starts up + gdbserver on the remote device, and gdb on the local device. + """ + # Wait until we see "PID" + pid = self._GetProcessId(process) + assert pid != 0 + # No longer need the logcat process. + process.kill() + # Disable python's processing of SIGINT while running gdb. Otherwise + # control-c doesn't work well in gdb. + signal.signal(signal.SIGINT, signal.SIG_IGN) + gdbserver_process = subprocess.Popen(self._CreateADBCommand(['shell', + 'gdbserver', + '--attach', + ':5039', + pid])) + atexit.register(_ExitIfNeeded, gdbserver_process) + + gdbinit_path = os.path.join(self.temp_gdb_dir, 'gdbinit') + _CreateGdbInit(self.temp_gdb_dir, gdbinit_path, self.paths.build_dir) + + # Wait a second for gdb to start up on the device. Without this the local + # gdb starts before the remote side has registered the port. + # TODO(sky): maybe we should try a couple of times and then give up? + time.sleep(1) + + local_gdb_process = subprocess.Popen([self._GetLocalGdbPath(), + "-x", + gdbinit_path], + cwd=self.temp_gdb_dir) + atexit.register(_ExitIfNeeded, local_gdb_process) + local_gdb_process.wait() + signal.signal(signal.SIGINT, signal.SIG_DFL) + + def StartActivity(self, + activity_name, + arguments, + stdout, + on_fifo_closed, + temp_gdb_dir=None): """ - STDOUT_PIPE = "/data/data/%s/stdout.fifo" % self.target_package + Starts the shell with the given |arguments|, directing output to |stdout|. + |on_fifo_closed| will be run if the FIFO can't be found or when it's closed. + |temp_gdb_dir| is set to a location with appropriate symlinks for gdb to + find when attached to the device's remote process on startup. + """ + assert self.device + arguments += self.shell_args cmd = self._CreateADBCommand([ 'shell', @@ -351,57 +254,81 @@ class AndroidShell(object): 'start', '-S', '-a', 'android.intent.action.VIEW', - '-n', '%s/%s.MojoShellActivity' % (self.target_package, - MOJO_SHELL_PACKAGE_NAME)]) - - parameters = [] - if stdout or on_application_stop: - subprocess.check_call(self._CreateADBCommand( - ['shell', 'rm', '-f', STDOUT_PIPE])) - parameters.append('--fifo-path=%s' % STDOUT_PIPE) - max_attempts = 5 - if '--wait-for-debugger' in arguments: - max_attempts = 200 - self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop, - max_attempts=max_attempts) - - # Extract map-origin arguments. - map_parameters, other_parameters = _Split(arguments, _IsMapOrigin) - parameters += other_parameters + '-n', '%s/%s.%s' % (self.target_package, + self.target_package, + activity_name)]) + + logcat_process = None + if temp_gdb_dir: + self.temp_gdb_dir = temp_gdb_dir + arguments.append('--wait-for-debugger') + # Remote debugging needs a port forwarded. + self.device.adb.Forward('tcp:5039', 'tcp:5039') + logcat_process = self.ShowLogs(stdout=subprocess.PIPE) + + fifo_path = "/data/data/%s/stdout.fifo" % self.target_package + subprocess.check_call(self._CreateADBCommand( + ['shell', 'rm', '-f', fifo_path])) + arguments.append('--fifo-path=%s' % fifo_path) + max_attempts = 200 if '--wait-for-debugger' in arguments else 5 + self._ReadFifo(fifo_path, stdout, on_fifo_closed, max_attempts) + + # Extract map-origin args and add the extras array with commas escaped. + parameters = [a for a in arguments if not a.startswith(MAPPING_PREFIX)] + map_parameters = [a for a in arguments if a.startswith(MAPPING_PREFIX)] parameters += self._StartHttpServerForOriginMappings(map_parameters) + parameters = [p.replace(',', '\,') for p in parameters] + cmd += ['--esa', '%s.extras' % self.target_package, ','.join(parameters)] - if parameters: - encodedParameters = json.dumps(parameters) - cmd += ['--es', 'encodedParameters', encodedParameters] - + atexit.register(self.kill) with open(os.devnull, 'w') as devnull: - subprocess.Popen(cmd, stdout=devnull).wait() + cmd_process = subprocess.Popen(cmd, stdout=devnull) + if logcat_process: + self._WaitForProcessIdAndStartGdb(logcat_process) + cmd_process.wait() - def StopShell(self): - """ - Stops the mojo shell. - """ - subprocess.check_call(self._CreateADBCommand(['shell', - 'am', - 'force-stop', - self.target_package])) - - def CleanLogs(self): - """ - Cleans the logs on the device. - """ - subprocess.check_call(self._CreateADBCommand(['logcat', '-c'])) + def kill(self): + """Stops the mojo shell; matches the Popen.kill method signature.""" + self.device.ForceStop(self.target_package) - def ShowLogs(self): - """ - Displays the log for the mojo shell. - - Returns the process responsible for reading the logs. - """ + def ShowLogs(self, stdout=sys.stdout): + """Displays the mojo shell logs and returns the process reading the logs.""" logcat = subprocess.Popen(self._CreateADBCommand([ 'logcat', '-s', ' '.join(LOGCAT_TAGS)]), - stdout=sys.stdout) + stdout=stdout) atexit.register(_ExitIfNeeded, logcat) return logcat + + +def _CreateGdbInit(tmp_dir, gdb_init_path, build_dir): + """ + Creates the gdbinit file. + + Args: + tmp_dir: the directory where the gdbinit and other files lives. + gdb_init_path: path to gdbinit + build_dir: path where build files are located. + """ + gdbinit = ('target remote localhost:5039\n' + 'def reload-symbols\n' + ' set solib-search-path %s:%s\n' + 'end\n' + 'def info-symbols\n' + ' info sharedlibrary\n' + 'end\n' + 'reload-symbols\n' + 'echo \\n\\n' + 'You are now in gdb and need to type continue (or c) to continue ' + 'execution.\\n' + 'gdb is in the directory %s\\n' + 'The following functions have been defined:\\n' + 'reload-symbols: forces reloading symbols. If after a crash you\\n' + 'still do not see symbols you likely need to create a link in\\n' + 'the directory you are in.\\n' + 'info-symbols: shows status of current shared libraries.\\n' + 'NOTE: you may need to type reload-symbols again after a ' + 'crash.\\n\\n' % (tmp_dir, build_dir, tmp_dir)) + with open(gdb_init_path, 'w') as f: + f.write(gdbinit) diff --git a/chromium/mojo/tools/mopy/config.py b/chromium/mojo/tools/mopy/config.py index ba43bd03fa7..8496d031d75 100644 --- a/chromium/mojo/tools/mopy/config.py +++ b/chromium/mojo/tools/mopy/config.py @@ -2,75 +2,55 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Build/test configurations, which are just dictionaries. This -"defines" the schema and provides some wrappers.""" - +import ast +import os.path import platform +import re import sys class Config(object): - """A Config is basically just a wrapper around a dictionary that species a - build/test configuration. The dictionary is accessible through the values - member.""" + """A Config contains a dictionary that species a build configuration.""" - # Valid values for target_os (None is also valid): + # Valid values for target_os: OS_ANDROID = "android" OS_CHROMEOS = "chromeos" OS_LINUX = "linux" OS_MAC = "mac" OS_WINDOWS = "windows" - # Valid values for target_cpu (None is also valid): + # Valid values for target_cpu: ARCH_X86 = "x86" ARCH_X64 = "x64" ARCH_ARM = "arm" - # Valid values for sanitizer (None is also valid): - SANITIZER_ASAN = "asan" - - # Standard values for test types (test types are arbitrary strings; other - # values are allowed). - TEST_TYPE_DEFAULT = "default" - TEST_TYPE_UNIT = "unit" - TEST_TYPE_PERF = "perf" - TEST_TYPE_INTEGRATION = "integration" - - def __init__(self, target_os=None, target_cpu=None, is_debug=True, - is_clang=None, sanitizer=None, dcheck_always_on=False, - apk_name="MojoRunner.apk", **kwargs): - """Constructs a Config with key-value pairs specified via keyword arguments. - If target_os is not specified, it will be set to the host OS.""" - + def __init__(self, build_dir=None, target_os=None, target_cpu=None, + is_debug=None, apk_name="MojoRunner.apk"): + """Function arguments take precedence over GN args and default values.""" assert target_os in (None, Config.OS_ANDROID, Config.OS_CHROMEOS, Config.OS_LINUX, Config.OS_MAC, Config.OS_WINDOWS) assert target_cpu in (None, Config.ARCH_X86, Config.ARCH_X64, - Config.ARCH_ARM) - assert isinstance(is_debug, bool) - assert is_clang is None or isinstance(is_clang, bool) - assert sanitizer in (None, Config.SANITIZER_ASAN) - if "test_types" in kwargs: - assert isinstance(kwargs["test_types"], list) - - self.values = {} - self.values["target_os"] = (self.GetHostOS() if target_os is None else - target_os) - - if target_cpu is None: - if target_os == Config.OS_ANDROID: - target_cpu = Config.ARCH_ARM - else: - target_cpu = self.GetHostCPUArch() - self.values["target_cpu"] = target_cpu - - self.values["is_debug"] = is_debug - self.values["is_clang"] = is_clang - self.values["sanitizer"] = sanitizer - self.values["dcheck_always_on"] = dcheck_always_on - self.values["apk_name"] = apk_name - - self.values.update(kwargs) + Config.ARCH_ARM) + assert is_debug in (None, True, False) + + self.values = { + "build_dir": build_dir, + "target_os": self.GetHostOS(), + "target_cpu": self.GetHostCPU(), + "is_debug": True, + "dcheck_always_on": False, + "is_asan": False, + "apk_name": apk_name, + } + + self._ParseGNArgs() + if target_os is not None: + self.values["target_os"] = target_os + if target_cpu is not None: + self.values["target_cpu"] = target_cpu + if is_debug is not None: + self.values["is_debug"] = is_debug @staticmethod def GetHostOS(): @@ -83,21 +63,44 @@ class Config(object): raise NotImplementedError("Unsupported host OS") @staticmethod - def GetHostCPUArch(): + def GetHostCPU(): # Derived from //native_client/pynacl/platform.py machine = platform.machine() if machine in ("x86", "x86-32", "x86_32", "x8632", "i386", "i686", "ia32", "32"): return Config.ARCH_X86 - if machine in ("x86-64", "amd64", "x86_64", "x8664", "64"): + if machine in ("x86-64", "amd64", "AMD64", "x86_64", "x8664", "64"): return Config.ARCH_X64 if machine.startswith("arm"): return Config.ARCH_ARM raise Exception("Cannot identify CPU arch: %s" % machine) + def _ParseGNArgs(self): + """Parse the gn config file from the build directory, if it exists.""" + TRANSLATIONS = { "true": "True", "false": "False", } + if self.values["build_dir"] is None: + return + gn_file = os.path.join(self.values["build_dir"], "args.gn") + if not os.path.isfile(gn_file): + return + + with open(gn_file, "r") as f: + for line in f: + line = re.sub("\s*#.*", "", line) + result = re.match("^\s*(\w+)\s*=\s*(.*)\s*$", line) + if result: + key = result.group(1) + value = result.group(2) + self.values[key] = ast.literal_eval(TRANSLATIONS.get(value, value)) + # Getters for standard fields ------------------------------------------------ @property + def build_dir(self): + """Build directory path.""" + return self.values["build_dir"] + + @property def target_os(self): """OS of the build/test target.""" return self.values["target_os"] @@ -118,21 +121,11 @@ class Config(object): return self.values["dcheck_always_on"] @property + def is_asan(self): + """Is ASAN build?""" + return self.values["is_asan"] + + @property def apk_name(self): """Name of the APK file to run""" return self.values["apk_name"] - - @property - def is_clang(self): - """Should use clang?""" - return self.values["is_clang"] - - @property - def sanitizer(self): - """Sanitizer to use, if any.""" - return self.values["sanitizer"] - - @property - def test_types(self): - """List of test types to run.""" - return self.values.get("test_types", [Config.TEST_TYPE_DEFAULT]) diff --git a/chromium/mojo/tools/mopy/dart_apptest.py b/chromium/mojo/tools/mopy/dart_apptest.py deleted file mode 100644 index 5e12b17417f..00000000000 --- a/chromium/mojo/tools/mopy/dart_apptest.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2014 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 - -_logging = logging.getLogger() - -from mopy import test_util -from mopy.print_process_error import print_process_error - - -# TODO(erg): Support android, launched services and fixture isolation. -def run_test(config, shell, apptest_dict, shell_args, apps_and_args=None): - """Runs a command line and checks the output for signs of gtest failure. - - Args: - config: The mopy.config.Config object for the build. - shell_args: The arguments for mojo_shell. - apps_and_args: A Dict keyed by application URL associated to the - application's specific arguments. - """ - apps_and_args = apps_and_args or {} - output = test_util.try_run_test(config, shell, shell_args, apps_and_args) - # Fail on output with dart unittests' "FAIL:"/"ERROR:" or a lack of "PASS:". - # The latter condition ensures failure on broken command lines or output. - # Check output instead of exit codes because mojo_shell always exits with 0. - if (not output or - '\nFAIL: ' in output or - '\nERROR: ' in output or - '\nPASS: ' not in output): - print "Failed test:" - print_process_error( - test_util.build_command_line(config, shell_args, apps_and_args), - output) - return "Failed test(s) in %r" % apptest_dict - _logging.debug("Succeeded with output:\n%s" % output) - return "Succeeded" diff --git a/chromium/mojo/tools/mopy/gn.py b/chromium/mojo/tools/mopy/gn.py deleted file mode 100644 index 560e8db78dc..00000000000 --- a/chromium/mojo/tools/mopy/gn.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2015 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. - -""" -GN-related configuration functions, e.g., to produce a Config object from a GN -args.gn file). -""" - - -import ast -import os.path -import re - -from .config import Config - - -def BuildDirectoryForConfig(config, src_root): - """ - Returns the build directory for the given configuration. - """ - subdir = "" - if config.target_os == Config.OS_ANDROID: - subdir += "android_" - if config.target_cpu != Config.ARCH_ARM: - subdir += config.target_cpu + "_" - elif config.target_os == Config.OS_CHROMEOS: - subdir += "chromeos_" - subdir += "Debug" if config.is_debug else "Release" - if config.sanitizer == Config.SANITIZER_ASAN: - subdir += "_asan" - if not(config.is_debug) and config.dcheck_always_on: - subdir += "_dcheck" - return os.path.join(src_root, "out", subdir) - - -def GNArgsForConfig(config): - """ - Return the arguments for gn for the given configuration. This function returns - a dictionary with boolean values as boolean. - """ - gn_args = {} - - gn_args["is_debug"] = bool(config.is_debug) - gn_args["is_asan"] = config.sanitizer == Config.SANITIZER_ASAN - - if config.is_clang is not None: - gn_args["is_clang"] = bool(config.is_clang) - else: - gn_args["is_clang"] = config.target_os not in (Config.OS_ANDROID, - Config.OS_WINDOWS) - - if config.values.get("use_goma"): - gn_args["use_goma"] = True - gn_args["goma_dir"] = config.values["goma_dir"] - else: - gn_args["use_goma"] = False - - gn_args["dcheck_always_on"] = config.dcheck_always_on - - gn_args["mojo_use_nacl"] = config.values.get("use_nacl", False) - - if config.target_os == Config.OS_ANDROID: - gn_args["os"] = "android" - elif config.target_os == Config.OS_CHROMEOS: - gn_args["os"] = "chromeos" - gn_args["use_glib"] = False - gn_args["use_system_harfbuzz"] = False - elif config.target_os == Config.OS_LINUX: - gn_args["use_aura"] = False - gn_args["use_glib"] = False - gn_args["use_system_harfbuzz"] = False - - gn_args["target_cpu"] = config.target_cpu - - extra_args = config.values.get("gn_args") - if extra_args: - for arg in extra_args.split(): - (name, val) = arg.split('=') - gn_args[name] = val - - return gn_args - - -def CommandLineForGNArgs(gn_args): - """ - Returns the list of gn arguments to use with the gn command line. - """ - def _ToCommandLine(key, value): - if type(value) is bool: - return "%s=%s" % (key, "true" if value else "false") - return "%s=\"%s\"" % (key, value) - return [_ToCommandLine(x, y) for x, y in gn_args.iteritems()] - - -def ConfigForGNArgs(args): - """ - Return the Config object for the given gn arguments. This function takes a - dictionary with boolean values as boolean. - """ - config_args = {} - config_args["is_debug"] = args.get("is_debug", False) - config_args["sanitizer"] = ( - Config.SANITIZER_ASAN if args.get("is_asan") else None) - config_args["is_clang"] = args.get("is_clang", False) - config_args["use_goma"] = args.get("use_goma", False) - if config_args["use_goma"]: - config_args["goma_dir"] = args.get("goma_dir") - config_args["use_nacl"] = args.get("mojo_use_nacl", False) - config_args["target_os"] = args.get("target_os") - config_args["target_cpu"] = args.get("target_cpu") - config_args["dcheck_always_on"] = args.get("dcheck_always_on") - return Config(**config_args) - - -def ParseGNConfig(build_dir): - """ - Parse the gn config file present in |build_dir|. This function returns a - dictionary with boolean values as boolean. - """ - TRANSLATIONS = { - "true": "True", - "false": "False", - } - gn_file = os.path.join(build_dir, "args.gn") - values = {} - with open(gn_file, "r") as f: - for line in f.readlines(): - line = re.sub("\s*#.*", "", line) - result = re.match("^\s*(\w+)\s*=\s*(.*)\s*$", line) - if result: - key = result.group(1) - value = result.group(2) - values[key] = ast.literal_eval(TRANSLATIONS.get(value, value)) - - # TODO(msw): The build dir is derived from GN args 'is_debug' and 'target_os'. - # The script should probably use its 'build_dir' argument instead. - if not "is_debug" in values: - values["is_debug"] = "Debug" in build_dir - - return values diff --git a/chromium/mojo/tools/mopy/gtest.py b/chromium/mojo/tools/mopy/gtest.py index 1cde8ecee23..b72aad36c99 100644 --- a/chromium/mojo/tools/mopy/gtest.py +++ b/chromium/mojo/tools/mopy/gtest.py @@ -4,129 +4,192 @@ import logging import os +import Queue import re +import subprocess import sys +import threading +import time -from mopy import test_util -from mopy.print_process_error import print_process_error - - -_logger = logging.getLogger() +from mopy.config import Config +from mopy.paths import Paths def set_color(): - """Run gtests with color if we're on a TTY (and we're not being told - explicitly what to do).""" + """Run gtests with color on TTY, unless its environment variable is set.""" if sys.stdout.isatty() and "GTEST_COLOR" not in os.environ: - _logger.debug("Setting GTEST_COLOR=yes") + logging.getLogger().debug("Setting GTEST_COLOR=yes") os.environ["GTEST_COLOR"] = "yes" -# TODO(vtl): The return value is bizarre. Should just make it either return -# True/False, or a list of failing fixtures. But the dart_apptest runner would -# also need to be updated in the same way. -def run_fixtures(config, shell, apptest_dict, apptest, isolate, test_args, - shell_args): - """Run the gtest fixtures in isolation.""" - - if not isolate: - if not RunApptestInShell(config, shell, apptest, test_args, shell_args): - return "Failed test(s) in %r" % apptest_dict - return "Succeeded" - - # List the apptest fixtures so they can be run independently for isolation. - fixtures = get_fixtures(config, shell, shell_args, apptest) - - if not fixtures: - return "Failed with no tests found." - - apptest_result = "Succeeded" - for fixture in fixtures: - apptest_args = test_args + ["--gtest_filter=%s" % fixture] - success = RunApptestInShell(config, shell, apptest, apptest_args, - shell_args) - - if not success: - apptest_result = "Failed test(s) in %r" % apptest_dict - return apptest_result +def run_apptest(config, shell, args, apptest, isolate): + """Run the apptest; optionally isolating fixtures across shell invocations. - -def run_test(config, shell, shell_args, apps_and_args=None): - """Runs a command line and checks the output for signs of gtest failure. + Returns the list of tests run and the list of failures. Args: - config: The mopy.config.Config object for the build. - shell_args: The arguments for mojo_shell. - apps_and_args: A Dict keyed by application URL associated to the - application's specific arguments. + config: The mopy.config.Config for the build. + shell: The mopy.android.AndroidShell, if Android is the target platform. + args: The arguments for the shell or apptest. + apptest: The application test URL. + isolate: True if the test fixtures should be run in isolation. """ - apps_and_args = apps_and_args or {} - output = test_util.try_run_test(config, shell, shell_args, apps_and_args) + tests = [apptest] + failed = [] + if not isolate: + # TODO(msw): Parse fixture-granular successes and failures in this case. + # TODO(msw): Retry fixtures that failed, not the entire apptest suite. + if not _run_apptest_with_retry(config, shell, args, apptest): + failed.append(apptest) + else: + tests = _get_fixtures(config, shell, args, apptest) + for fixture in tests: + arguments = args + ["--gtest_filter=%s" % fixture] + if not _run_apptest_with_retry(config, shell, arguments, apptest): + failed.append(fixture) + # Abort when 20 fixtures, or a tenth of the apptest fixtures, have failed. + # base::TestLauncher does this for timeouts and unknown results. + if len(failed) >= max(20, len(tests) / 10): + print "Too many failing fixtures (%d), exiting now." % len(failed) + return (tests, failed + [apptest + " aborted for excessive failures."]) + return (tests, failed) + + +# TODO(msw): Determine proper test retry counts; allow configuration. +def _run_apptest_with_retry(config, shell, args, apptest, try_count=3): + """Runs an apptest, retrying on failure; returns True if any run passed.""" + for try_number in range(try_count): + if _run_apptest(config, shell, args, apptest): + return True + print "Failed %s/%s test run attempts." % (try_number + 1, try_count) + return False + + +def _run_apptest(config, shell, args, apptest): + """Runs an apptest and checks the output for signs of gtest failure.""" + command = _build_command_line(config, args, apptest) + logging.getLogger().debug("Command: %s" % " ".join(command)) + start_time = time.time() + + try: + output = _run_test_with_timeout(config, shell, args, apptest) + except Exception as e: + _print_exception(command, e) + return False + # Fail on output with gtest's "[ FAILED ]" or a lack of "[ PASSED ]". # The latter condition ensures failure on broken command lines or output. - # Check output instead of exit codes because mojo_shell always exits with 0. - if (output is None or - (output.find("[ FAILED ]") != -1 or output.find("[ PASSED ]") == -1)): - print "Failed test:" - print_process_error( - test_util.build_command_line(config, shell_args, apps_and_args), - output) + # Check output instead of exit codes because mojo shell always exits with 0. + if output.find("[ FAILED ]") != -1 or output.find("[ PASSED ]") == -1: + _print_exception(command, output) return False - _logger.debug("Succeeded with output:\n%s" % output) - return True - -def get_fixtures(config, shell, shell_args, apptest): - """Returns the "Test.Fixture" list from an apptest using mojo_shell. + ms = int(round(1000 * (time.time() - start_time))) + logging.getLogger().debug("Passed with output (%d ms):\n%s" % (ms, output)) + return True - Tests are listed by running the given apptest in mojo_shell and passing - --gtest_list_tests. The output is parsed and reformatted into a list like - [TestSuite.TestFixture, ... ] - An empty list is returned on failure, with errors logged. - Args: - config: The mopy.config.Config object for the build. - apptest: The URL of the test application to run. - """ +def _get_fixtures(config, shell, args, apptest): + """Returns an apptest's "Suite.Fixture" list via --gtest_list_tests output.""" + arguments = args + ["--gtest_list_tests"] + command = _build_command_line(config, arguments, apptest) + logging.getLogger().debug("Command: %s" % " ".join(command)) try: - apps_and_args = {apptest: ["--gtest_list_tests"]} - list_output = test_util.run_test(config, shell, shell_args, apps_and_args) - _logger.debug("Tests listed:\n%s" % list_output) - return _gtest_list_tests(list_output) + tests = _run_test_with_timeout(config, shell, arguments, apptest) + logging.getLogger().debug("Tests for %s:\n%s" % (apptest, tests)) + # Remove log lines from the output and ensure it matches known formatting. + tests = re.sub("^(\[|WARNING: linker:).*\n", "", tests, flags=re.MULTILINE) + if not re.match("^(\w*\.\r?\n( \w*\r?\n)+)+", tests): + raise Exception("Unrecognized --gtest_list_tests output:\n%s" % tests) + tests = tests.split("\n") + test_list = [] + for line in tests: + if not line: + continue + if line[0] != " ": + suite = line.strip() + continue + test_list.append(suite + line.strip()) + return test_list except Exception as e: - print "Failed to get test fixtures:" - print_process_error( - test_util.build_command_line(config, shell_args, apps_and_args), e) + _print_exception(command, e) return [] -def _gtest_list_tests(gtest_list_tests_output): - """Returns a list of strings formatted as TestSuite.TestFixture from the - output of running --gtest_list_tests on a GTEST application.""" - - # Remove log lines. - gtest_list_tests_output = re.sub("^(\[|WARNING: linker:).*\n", - "", - gtest_list_tests_output, - flags=re.MULTILINE) - - if not re.match("^(\w*\.\r?\n( \w*\r?\n)+)+", gtest_list_tests_output): - raise Exception("Unrecognized --gtest_list_tests output:\n%s" % - gtest_list_tests_output) - - output_lines = gtest_list_tests_output.split("\n") - - test_list = [] - for line in output_lines: - if not line: - continue - if line[0] != " ": - suite = line.strip() - continue - test_list.append(suite + line.strip()) - - return test_list - - -def RunApptestInShell(config, shell, application, application_args, shell_args): - return run_test(config, shell, shell_args, {application: application_args}) +def _print_exception(command_line, exception): + """Print a formatted exception raised from a failed command execution.""" + exit_code = "" + if hasattr(exception, 'returncode'): + exit_code = " (exit code %d)" % exception.returncode + print "\n[ FAILED ] Command%s: %s" % (exit_code, " ".join(command_line)) + print 72 * "-" + if hasattr(exception, 'output'): + print exception.output + print str(exception) + print 72 * "-" + + +def _build_command_line(config, args, apptest): + """Build the apptest command line. This value isn't executed on Android.""" + paths = Paths(config) + # On Linux, always run tests with xvfb, but not for --gtest_list_tests. + use_xvfb = (config.target_os == Config.OS_LINUX and + not "--gtest_list_tests" in args) + prefix = [paths.xvfb, paths.build_dir] if use_xvfb else [] + return prefix + [paths.mojo_runner] + args + [apptest] + + +# TODO(msw): Determine proper test timeout durations (starting small). +def _run_test_with_timeout(config, shell, args, apptest, timeout_in_seconds=10): + """Run the test with a timeout; return the output or raise an exception.""" + result = Queue.Queue() + thread = threading.Thread(target=_run_test, + args=(config, shell, args, apptest, result)) + thread.start() + process_or_shell = result.get() + thread.join(timeout_in_seconds) + + if thread.is_alive(): + try: + process_or_shell.kill() + except OSError: + pass # The process may have ended after checking |is_alive|. + return "Error: Test timeout after %s seconds" % timeout_in_seconds + + if not result.empty(): + (output, exception) = result.get() + if exception: + raise Exception("%s%s%s" % (output, "\n" if output else "", exception)) + return output + + return "Error: Test exited with no output." + + +def _run_test(config, shell, args, apptest, result): + """Run the test and put the output and any exception in |result|.""" + output = "" + exception = "" + try: + if (config.target_os != Config.OS_ANDROID): + command = _build_command_line(config, args, apptest) + process = subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + result.put(process) + process.wait() + if not process.poll(): + output = str(process.stdout.read()) + else: + exception = "Error: Test exited with code: %d" % process.returncode + else: + assert shell + result.put(shell) + (r, w) = os.pipe() + with os.fdopen(r, "r") as rf: + with os.fdopen(w, "w") as wf: + arguments = args + [apptest] + shell.StartActivity('MojoShellActivity', arguments, wf, wf.close) + output = rf.read() + except Exception as e: + output = e.output if hasattr(e, 'output') else "" + exception = str(e) + result.put((output, exception)) diff --git a/chromium/mojo/tools/mopy/log.py b/chromium/mojo/tools/mopy/log.py deleted file mode 100644 index af572320818..00000000000 --- a/chromium/mojo/tools/mopy/log.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2015 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. - -"""Logging utilities, for use with the standard logging module.""" - - -import logging - - -def InitLogging(verbose_count): - """Ensures that the logger (obtained via logging.getLogger(), as usual) is - initialized, with the log level set as appropriate for |verbose_count| - instances of --verbose on the command line.""" - - assert(verbose_count >= 0) - if verbose_count == 0: - level = logging.WARNING - elif verbose_count == 1: - level = logging.INFO - else: # verbose_count >= 2 - level = logging.DEBUG - - logging.basicConfig(format="%(relativeCreated).3f:%(levelname)s:%(message)s") - logger = logging.getLogger() - logger.setLevel(level) - - logger.debug("Initialized logging: verbose_count=%d, level=%d" % - (verbose_count, level)) diff --git a/chromium/mojo/tools/mopy/paths.py b/chromium/mojo/tools/mopy/paths.py index 751629a3c85..767732921bd 100644 --- a/chromium/mojo/tools/mopy/paths.py +++ b/chromium/mojo/tools/mopy/paths.py @@ -5,43 +5,41 @@ import os from .config import Config -from .gn import BuildDirectoryForConfig + class Paths(object): """Provides commonly used paths""" - def __init__(self, config=None, build_dir=None): - """Specify either a config or a build_dir to generate paths to binary - artifacts.""" + def __init__(self, config): + """Generate paths to binary artifacts from a Config object.""" self.src_root = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir, os.pardir, os.pardir)) self.mojo_dir = os.path.join(self.src_root, "mojo") - self.adb_path = os.path.join(self.src_root, 'third_party', 'android_tools', - 'sdk', 'platform-tools', 'adb') - - if config: - self.build_dir = BuildDirectoryForConfig(config, self.src_root) - elif build_dir is not None: - self.build_dir = os.path.abspath(build_dir) - else: - self.build_dir = None - if self.build_dir is not None: - self.mojo_shell_path = os.path.join(self.build_dir, "mojo_runner") - # TODO(vtl): Use the host OS here, since |config| may not be available. - # In any case, if the target is Windows, but the host isn't, using - # |os.path| isn't correct.... - if Config.GetHostOS() == Config.OS_WINDOWS: - self.mojo_shell_path += ".exe" - if config and config.target_os == Config.OS_ANDROID: - self.target_mojo_shell_path = os.path.join(self.build_dir, - "apks", - config.apk_name) - else: - self.target_mojo_shell_path = self.mojo_shell_path - else: - self.mojo_shell_path = None - self.target_mojo_shell_path = None + self.build_dir = config.build_dir + if self.build_dir is None: + subdir = "" + if config.target_os == Config.OS_ANDROID: + subdir += "android_" + if config.target_cpu != Config.ARCH_ARM: + subdir += config.target_cpu + "_" + elif config.target_os == Config.OS_CHROMEOS: + subdir += "chromeos_" + subdir += "Debug" if config.is_debug else "Release" + if config.is_asan: + subdir += "_asan" + if not(config.is_debug) and config.dcheck_always_on: + subdir += "_dcheck" + self.build_dir = os.path.join(self.src_root, "out", subdir) + + self.mojo_runner = os.path.join(self.build_dir, "mojo_runner") + if config.target_os == Config.OS_WINDOWS: + self.mojo_runner += ".exe" + if config.target_os == Config.OS_ANDROID: + self.apk_path = os.path.join(self.build_dir, "apks", config.apk_name) + self.mojo_runner = os.path.join(self.src_root, "mojo", "tools", + "android_mojo_shell.py") + self.xvfb = os.path.join(self.src_root, "testing", "xvfb.py") def RelPath(self, path): """Returns the given path, relative to the current directory.""" @@ -50,20 +48,3 @@ class Paths(object): def SrcRelPath(self, path): """Returns the given path, relative to self.src_root.""" return os.path.relpath(path, self.src_root) - - def FileFromUrl(self, url): - """Given an app URL (<scheme>:<appname>), return 'build_dir/appname.mojo'. - If self.build_dir is None, just return appname.mojo - """ - (_, name) = url.split(':') - if self.build_dir: - return os.path.join(self.build_dir, name + '.mojo') - return name + '.mojo' - - @staticmethod - def IsValidAppUrl(url): - """Returns False if url is malformed, True otherwise.""" - try: - return len(url.split(':')) == 2 - except ValueError: - return False diff --git a/chromium/mojo/tools/mopy/print_process_error.py b/chromium/mojo/tools/mopy/print_process_error.py deleted file mode 100644 index ec565d14e40..00000000000 --- a/chromium/mojo/tools/mopy/print_process_error.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2014 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. - -def print_process_error(command_line, error): - """Properly format an exception raised from a failed command execution.""" - - if command_line: - print 'Failed command: %r' % command_line - else: - print 'Failed command:' - print 72 * '-' - - if hasattr(error, 'returncode'): - print ' with exit code %d' % error.returncode - print 72 * '-' - - if hasattr(error, 'output'): - print error.output - else: - print error - print 72 * '-' diff --git a/chromium/mojo/tools/mopy/test_util.py b/chromium/mojo/tools/mopy/test_util.py deleted file mode 100644 index cb8886a42ef..00000000000 --- a/chromium/mojo/tools/mopy/test_util.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2015 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 subprocess -import time - -from mopy.config import Config -from mopy.paths import Paths -from mopy.print_process_error import print_process_error - - -_logger = logging.getLogger() - - -def build_shell_arguments(shell_args, apps_and_args=None): - """Build the list of arguments for the shell. |shell_args| are the base - arguments, |apps_and_args| is a dictionary that associates each application to - its specific arguments|. Each app included will be run by the shell. - """ - result = shell_args[:] - if apps_and_args: - # TODO(msw): Mojo's script uses --args-for; Chromium lacks support for that. - for app_and_args in apps_and_args.items(): - result += app_and_args[1] - result += apps_and_args.keys() - return result - - -def get_shell_executable(config): - paths = Paths(config=config) - if config.target_os == Config.OS_ANDROID: - return os.path.join(paths.src_root, "mojo", "tools", - "android_mojo_shell.py") - else: - return paths.mojo_shell_path - - -def build_command_line(config, shell_args, apps_and_args): - executable = get_shell_executable(config) - return "%s %s" % (executable, " ".join(["%r" % x for x in - build_shell_arguments( - shell_args, apps_and_args)])) - - -def run_test_android(shell, shell_args, apps_and_args): - """Run the given test on the single/default android device.""" - assert shell - (r, w) = os.pipe() - with os.fdopen(r, "r") as rf: - with os.fdopen(w, "w") as wf: - arguments = build_shell_arguments(shell_args, apps_and_args) - _logger.debug("Starting shell with arguments: %s" % arguments) - start_time = time.time() - # TODO(vtl): Do more logging in lower layers. - shell.StartShell(arguments, wf, wf.close) - rv = rf.read() - run_time = time.time() - start_time - _logger.debug("Shell completed") - # Only log if it took more than 3 seconds. - if run_time >= 3: - _logger.info("Shell test took %.3f seconds; arguments: %s" % - (run_time, arguments)) - return rv - - -def run_test(config, shell, shell_args, apps_and_args): - """Run the given test.""" - if (config.target_os == Config.OS_ANDROID): - return run_test_android(shell, shell_args, apps_and_args) - - executable = get_shell_executable(config) - command = ([executable] + build_shell_arguments(shell_args, apps_and_args)) - _logger.debug("Starting: %s" % " ".join(command)) - start_time = time.time() - rv = subprocess.check_output(command, stderr=subprocess.STDOUT) - run_time = time.time() - start_time - _logger.debug("Completed: %s" % " ".join(command)) - # Only log if it took more than 1 second. - if run_time >= 1: - _logger.info("Test took %.3f seconds: %s" % (run_time, " ".join(command))) - return rv - - -def try_run_test(config, shell, shell_args, apps_and_args): - """Returns the output of a command line or an empty string on error.""" - command_line = build_command_line(config, shell_args, apps_and_args) - _logger.debug("Running command line: %s" % command_line) - try: - return run_test(config, shell, shell_args, apps_and_args) - except Exception as e: - print_process_error(command_line, e) - return None diff --git a/chromium/mojo/tools/rev_sdk.py b/chromium/mojo/tools/rev_sdk.py index a33c37eb399..db810342512 100755 --- a/chromium/mojo/tools/rev_sdk.py +++ b/chromium/mojo/tools/rev_sdk.py @@ -23,6 +23,8 @@ sdk_dirs_to_clone = [ sdk_dirs_to_not_clone = [ "mojo/public/cpp/application", "mojo/public/interfaces/application", + "mojo/public/interfaces/network", + "mojo/public/java/application", ] # Individual files to preserve within the target repository during roll. These @@ -42,8 +44,9 @@ for sdk_dir in sdk_dirs_to_clone: sdk_dir_in_chromium = os.path.join(sdk_prefix_in_chromium, sdk_dir) dirs_to_clone[sdk_dir] = sdk_dir_in_chromium -def rev(source_dir, chromium_dir): - src_commit = system(["git", "show-ref", "HEAD", "-s"], cwd=source_dir).strip() +def rev(source_dir, chromium_dir, mojo_revision): + src_commit = system(["git", "rev-parse", mojo_revision], + cwd=source_dir).strip() for input_dir, dest_dir in dirs_to_clone.iteritems(): if os.path.exists(os.path.join(chromium_dir, dest_dir)): @@ -88,8 +91,10 @@ def rev(source_dir, chromium_dir): commit("Update mojo sdk to rev " + src_commit, cwd=chromium_dir) -if len(sys.argv) != 2: - print "usage: rev_sdk.py <mojo source dir>" +if len(sys.argv) < 2: + print "usage: rev_sdk.py <mojo source dir> [<mojo revision>]" sys.exit(1) -rev(sys.argv[1], chromium_root_dir) +# Allow override of the roll revision. +revision = sys.argv[2] if len(sys.argv) == 3 else 'origin/HEAD' +rev(sys.argv[1], chromium_root_dir, revision) |