summaryrefslogtreecommitdiffstats
path: root/mojo
diff options
context:
space:
mode:
authormsw <msw@chromium.org>2015-04-22 16:32:11 -0700
committerCommit bot <commit-bot@chromium.org>2015-04-22 23:33:17 +0000
commitad5ece3abccd3aeb6b2abd0d5b61d7036b342c8b (patch)
treebe47dc70b934f63a61643f53570ddbabd400ddbf /mojo
parent7b9ba2e6b4f149845360a6e0f3fa4b0ac459e6be (diff)
downloadchromium_src-ad5ece3abccd3aeb6b2abd0d5b61d7036b342c8b.zip
chromium_src-ad5ece3abccd3aeb6b2abd0d5b61d7036b342c8b.tar.gz
chromium_src-ad5ece3abccd3aeb6b2abd0d5b61d7036b342c8b.tar.bz2
Update apptest_runner.py from the Mojo repository.
This adds Android support and optional fixture isolation. It also has many fixes/improvements over our old script. It also supports dart apptests, but we don't need that. Patch Set 3 is a straight copy of needed Mojo repo files. Patch Set 4+ diverges as needed from the Mojo scripts. (nix --args-for & devtools.py; mv android.py; GN is_debug hack) TODO: Optionally flatten/nix dependencies and dart support. TODO: Use the build_dir arg instead of rebuilding it from gn args. BUG=479230 TEST=Bot continues passing apptests step. R=ben@chromium.org Review URL: https://codereview.chromium.org/996523003 Cr-Commit-Position: refs/heads/master@{#326399}
Diffstat (limited to 'mojo')
-rwxr-xr-xmojo/tools/apptest_runner.py110
-rw-r--r--mojo/tools/data/apptests29
-rw-r--r--mojo/tools/gtest.py107
-rw-r--r--mojo/tools/mopy/android.py503
-rw-r--r--mojo/tools/mopy/config.py2
-rw-r--r--mojo/tools/mopy/dart_apptest.py38
-rw-r--r--mojo/tools/mopy/gn.py6
-rw-r--r--mojo/tools/mopy/gtest.py132
-rw-r--r--mojo/tools/mopy/log.py29
-rw-r--r--mojo/tools/mopy/paths.py5
-rw-r--r--mojo/tools/mopy/print_process_error.py22
-rw-r--r--mojo/tools/mopy/test_util.py95
12 files changed, 734 insertions, 344 deletions
diff --git a/mojo/tools/apptest_runner.py b/mojo/tools/apptest_runner.py
index 9ef7b7a..ddc0830 100755
--- a/mojo/tools/apptest_runner.py
+++ b/mojo/tools/apptest_runner.py
@@ -1,81 +1,91 @@
#!/usr/bin/env python
-# Copyright 2015 The Chromium Authors. All rights reserved.
+# 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.
"""A test runner for gtest application tests."""
import argparse
-import ast
import logging
-import os
import sys
-_logging = logging.getLogger()
+from mopy import dart_apptest
+from mopy import gtest
+# TODO(msw): Mojo's script pulls in android.py via mojo/devtools/common/pylib.
+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
-import gtest
+
+_logger = logging.getLogger()
def main():
- logging.basicConfig()
- # Uncomment to debug:
- #_logging.setLevel(logging.DEBUG)
+ 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")
+ args = parser.parse_args()
- parser = argparse.ArgumentParser(description='A test runner for gtest '
- 'application tests.')
+ InitLogging(args.verbose_count)
+ config = ConfigForGNArgs(ParseGNConfig(args.build_dir))
- parser.add_argument('apptest_list_file', type=file,
- help='A file listing apptests to run.')
- parser.add_argument('build_dir', type=str,
- help='The build output directory.')
- args = parser.parse_args()
+ _logger.debug("Test list file: %s", args.test_list_file)
+ execution_globals = {"config": config}
+ exec args.test_list_file in execution_globals
+ test_list = execution_globals["tests"]
+ _logger.debug("Test list: %s" % test_list)
- apptest_list = ast.literal_eval(args.apptest_list_file.read())
- _logging.debug("Test list: %s" % apptest_list)
+ extra_args = []
+ 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(fixed_port=False))
+ else:
+ shell = None
gtest.set_color()
- mojo_shell_path = os.path.join(args.build_dir, "mojo_shell")
exit_code = 0
- for apptest_dict in apptest_list:
- if apptest_dict.get("disabled"):
- continue
-
- apptest = apptest_dict["test"]
- apptest_args = apptest_dict.get("test-args", [])
- shell_args = apptest_dict.get("shell-args", [])
-
- print "Running " + apptest + "...",
+ 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()
- # List the apptest fixtures so they can be run independently for isolation.
- # TODO(msw): Run some apptests without fixture isolation?
- fixtures = gtest.get_fixtures(mojo_shell_path, apptest)
-
- if not fixtures:
- print "Failed with no tests found."
+ 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
- continue
-
- apptest_result = "Succeeded"
- for fixture in fixtures:
- args_for_apptest = " ".join(["--gtest_filter=" + fixture] + apptest_args)
-
- success = RunApptestInShell(mojo_shell_path, apptest,
- shell_args + [args_for_apptest])
-
- if not success:
- apptest_result = "Failed test(s) in %r" % apptest_dict
- exit_code = 1
-
print apptest_result
+ _logger.info("Completed: %s" % test_name)
return exit_code
-def RunApptestInShell(mojo_shell_path, apptest, shell_args):
- return gtest.run_test([mojo_shell_path, apptest] + shell_args)
-
-
if __name__ == '__main__':
sys.exit(main())
diff --git a/mojo/tools/data/apptests b/mojo/tools/data/apptests
index 44ed41f..06ac22d 100644
--- a/mojo/tools/data/apptests
+++ b/mojo/tools/data/apptests
@@ -1,6 +1,30 @@
# This file contains a list of Mojo gtest unit tests.
-# This must be a valid python dictionary.
-[
+#
+# This must be valid Python. It may use the |config| global that will be a
+# mopy.config.Config object, and must set a |tests| global that will contain the
+# list of tests to run.
+#
+# The entries in |tests| are dictionaries of the following form:
+# {
+# # Required URL for apptest.
+# "test": "mojo:test_app_url",
+# # 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".
+# "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"],
+# }
+#
+# TODO(vtl|msw): Add a way of specifying data dependencies.
+
+tests = [
{
"test": "mojo:clipboard_apptests",
},
@@ -17,6 +41,7 @@
#},
{
"test": "mojo:view_manager_apptests",
+ "type": "gtest_isolated",
"shell-args": ["--url-mappings=mojo:window_manager=mojo:test_window_manager"]
},
{
diff --git a/mojo/tools/gtest.py b/mojo/tools/gtest.py
deleted file mode 100644
index d84c8e5..0000000
--- a/mojo/tools/gtest.py
+++ /dev/null
@@ -1,107 +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 re
-import subprocess
-import sys
-
-_logging = logging.getLogger()
-
-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 * '-'
-
-def set_color():
- """Run gtests with color if we're on a TTY (and we're not being told
- explicitly what to do)."""
- if sys.stdout.isatty() and 'GTEST_COLOR' not in os.environ:
- _logging.debug("Setting GTEST_COLOR=yes")
- os.environ['GTEST_COLOR'] = 'yes'
-
-
-def _try_command_line(command_line):
- """Returns the output of a command line or an empty string on error."""
- _logging.debug("Running command line: %s" % command_line)
- try:
- return subprocess.check_output(command_line, stderr=subprocess.STDOUT)
- except Exception as e:
- _print_process_error(command_line, e)
- return None
-
-
-def run_test(command_line):
- """Runs a command line and checks the output for signs of gtest failure."""
- output = _try_command_line(command_line)
- # 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(command_line, output)
- return False
- _logging.debug("Succeeded with output:\n%s" % output)
- return True
-
-
-def get_fixtures(mojo_shell, apptest):
- """Returns the "Test.Fixture" list from an apptest using mojo_shell.
-
- 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.
- """
- command = [mojo_shell, "--gtest_list_tests", apptest]
- try:
- list_output = subprocess.check_output(command, stderr=subprocess.STDOUT)
- _logging.debug("Tests listed:\n%s" % list_output)
- return _gtest_list_tests(list_output)
- except Exception as e:
- print "Failed to get test fixtures:"
- _print_process_error(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("^\[.*\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
diff --git a/mojo/tools/mopy/android.py b/mojo/tools/mopy/android.py
index 6584b9f..374ebb08 100644
--- a/mojo/tools/mopy/android.py
+++ b/mojo/tools/mopy/android.py
@@ -3,8 +3,13 @@
# 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
@@ -17,9 +22,6 @@ import urlparse
import SimpleHTTPServer
import SocketServer
-from mopy.config import Config
-from mopy.paths import Paths
-
# Tags used by the mojo shell application logs.
LOGCAT_TAGS = [
@@ -31,11 +33,30 @@ LOGCAT_TAGS = [
'chromium',
]
-ADB_PATH = os.path.join(Paths().src_root, 'third_party', 'android_tools', 'sdk',
- 'platform-tools', 'adb')
-
MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell'
+MAPPING_PREFIX = '--map-origin='
+
+DEFAULT_BASE_PORT = 31337
+
+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):
"""
@@ -58,6 +79,69 @@ def _GetHandlerClassForPath(base_path):
|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))
@@ -69,9 +153,26 @@ def _GetHandlerClassForPath(base_path):
"""
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.
@@ -80,186 +181,228 @@ def _ExitIfNeeded(process):
process.kill()
-def _ReadFifo(fifo_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.
+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 Run():
- def _WaitForFifo():
- command = [ADB_PATH, 'shell', 'test -e "%s"; echo $?' % fifo_path]
- for _ in xrange(max_attempts):
- if subprocess.check_output(command)[0] == '0':
- return
- time.sleep(1)
+ def __init__(
+ self, shell_apk_path, local_dir=None, adb_path="adb", target_device=None):
+ self.shell_apk_path = shell_apk_path
+ self.adb_path = adb_path
+ self.local_dir = local_dir
+ self.target_device = target_device
+
+ def _CreateADBCommand(self, args):
+ adb_command = [self.adb_path]
+ if self.target_device:
+ adb_command.extend(['-s', self.target_device])
+ adb_command.extend(args)
+ return adb_command
+
+ def _ReadFifo(self, fifo_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.
+ """
+ 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':
+ return
+ time.sleep(1)
+ if on_fifo_closed:
+ on_fifo_closed()
+ raise Exception("Unable to find fifo.")
+ _WaitForFifo()
+ stdout_cat = subprocess.Popen(self._CreateADBCommand([
+ 'shell',
+ 'cat',
+ fifo_path]),
+ stdout=pipe)
+ atexit.register(_ExitIfNeeded, stdout_cat)
+ stdout_cat.wait()
if on_fifo_closed:
on_fifo_closed()
- raise Exception("Unable to find fifo.")
- _WaitForFifo()
- stdout_cat = subprocess.Popen([ADB_PATH,
- 'shell',
- 'cat',
- fifo_path],
- stdout=pipe)
- atexit.register(_ExitIfNeeded, stdout_cat)
- stdout_cat.wait()
- if on_fifo_closed:
- on_fifo_closed()
-
- thread = threading.Thread(target=Run, name="StdoutRedirector")
- thread.start()
-
-
-def _MapPort(device_port, host_port):
- """
- Maps the device port to the host port. If |device_port| is 0, a random
- available port is chosen. Returns the device port.
- """
- def _FindAvailablePortOnDevice():
- opened = subprocess.check_output([ADB_PATH, '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([ADB_PATH,
- "reverse",
- "tcp:%d" % device_port,
- "tcp:%d" % host_port]).wait()
- def _UnmapPort():
- subprocess.Popen([ADB_PATH, "reverse", "--remove", "tcp:%d" % device_port])
- atexit.register(_UnmapPort)
- return device_port
-
-
-def StartHttpServerForDirectory(path):
- """Starts an http server serving files from |path|. Returns the local url."""
- print 'starting http for', 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 'local port=', httpd.server_address[1]
- return 'http://127.0.0.1:%d/' % _MapPort(0, httpd.server_address[1])
-
-
-def PrepareShellRun(config, origin=None):
- """ Prepares for StartShell: runs adb as root and installs the apk. If no
- --origin is specified, 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."""
- build_dir = Paths(config).build_dir
-
- subprocess.check_call([ADB_PATH, 'root'])
- apk_path = os.path.join(build_dir, 'apks', 'MojoShell.apk')
- subprocess.check_call(
- [ADB_PATH, 'install', '-r', apk_path, '-i', MOJO_SHELL_PACKAGE_NAME])
- atexit.register(StopShell)
-
- extra_shell_args = []
- origin_url = origin if origin else StartHttpServerForDirectory(build_dir)
- extra_shell_args.append("--origin=" + origin_url)
-
- return extra_shell_args
-
-
-def _StartHttpServerForOriginMapping(mapping):
- """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 urlparse.urlparse(dest)[0]:
- return mapping
- # Assume the destination is a local file. Start a local server that redirects
- # to it.
- localUrl = StartHttpServerForDirectory(dest)
- print 'started server at %s for %s' % (dest, localUrl)
- return parts[0] + '=' + localUrl
-
-
-def _StartHttpServerForOriginMappings(arg):
- """Calls _StartHttpServerForOriginMapping for every --map-origin argument."""
- mapping_prefix = '--map-origin='
- if not arg.startswith(mapping_prefix):
- return arg
- return mapping_prefix + ','.join([_StartHttpServerForOriginMapping(value)
- for value in arg[len(mapping_prefix):].split(',')])
-
-
-def StartShell(arguments, stdout=None, on_application_stop=None):
- """
- 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.
- """
- STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
-
- cmd = [ADB_PATH,
- 'shell',
- 'am',
- 'start',
- '-W',
- '-S',
- '-a', 'android.intent.action.VIEW',
- '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]
-
- parameters = []
- if stdout or on_application_stop:
- subprocess.check_call([ADB_PATH, 'shell', 'rm', STDOUT_PIPE])
- parameters.append('--fifo-path=%s' % STDOUT_PIPE)
- _ReadFifo(STDOUT_PIPE, stdout, on_application_stop)
- # The origin has to be specified whether it's local or external.
- assert any("--origin=" in arg for arg in arguments)
- parameters += [_StartHttpServerForOriginMappings(arg) for arg in arguments]
-
- if parameters:
- encodedParameters = json.dumps(parameters)
- cmd += [ '--es', 'encodedParameters', encodedParameters]
-
- with open(os.devnull, 'w') as devnull:
- subprocess.Popen(cmd, stdout=devnull).wait()
-
-
-def StopShell():
- """
- Stops the mojo shell.
- """
- subprocess.check_call(
- [ADB_PATH, 'shell', 'am', 'force-stop', MOJO_SHELL_PACKAGE_NAME])
+ thread = threading.Thread(target=Run, name="StdoutRedirector")
+ thread.start()
+ def _MapPort(self, device_port, host_port):
+ """
+ Maps the device port to the host port. If |device_port| is 0, a random
+ available port is chosen. Returns the device port.
+ """
+ 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
+ print 'starting http for', 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 'local port=%d' % 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 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)
+ return parts[0] + '=' + localUrl
+
+ def _StartHttpServerForOriginMappings(self, map_parameters, fixed_port):
+ """Calls _StartHttpServerForOriginMapping for every --map-origin
+ argument."""
+ if not map_parameters:
+ return []
+
+ original_values = list(itertools.chain(
+ *map(lambda x: x[len(MAPPING_PREFIX):].split(','), map_parameters)))
+ sorted(original_values)
+ result = []
+ for i, value in enumerate(original_values):
+ result.append(self._StartHttpServerForOriginMapping(
+ value, DEFAULT_BASE_PORT + 1 + i if fixed_port else 0))
+ return [MAPPING_PREFIX + ','.join(result)]
+
+ def PrepareShellRun(self, origin=None, fixed_port=True):
+ """ Prepares for StartShell: runs adb as root and installs the apk. If no
+ --origin is specified, 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',
+ MOJO_SHELL_PACKAGE_NAME]))
+ atexit.register(self.StopShell)
+
+ extra_shell_args = []
+ origin_url = origin if origin else self._StartHttpServerForDirectory(
+ self.local_dir, DEFAULT_BASE_PORT if fixed_port else 0)
+ extra_shell_args.append("--origin=" + origin_url)
+
+ return extra_shell_args
+
+ def StartShell(self,
+ arguments,
+ stdout=None,
+ on_application_stop=None,
+ fixed_port=True):
+ """
+ Starts the mojo shell, passing it the given arguments.
-def CleanLogs():
- """
- Cleans the logs on the device.
- """
- subprocess.check_call([ADB_PATH, 'logcat', '-c'])
+ The |arguments| list must contain the "--origin=" arg from PrepareShellRun.
+ If |stdout| is not None, it should be a valid argument for subprocess.Popen.
+ """
+ STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
+
+ cmd = self._CreateADBCommand([
+ 'shell',
+ 'am',
+ 'start',
+ '-S',
+ '-a', 'android.intent.action.VIEW',
+ '-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME])
+
+ parameters = []
+ if stdout or on_application_stop:
+ subprocess.check_call(self._CreateADBCommand(
+ ['shell', 'rm', STDOUT_PIPE]))
+ parameters.append('--fifo-path=%s' % STDOUT_PIPE)
+ self._ReadFifo(STDOUT_PIPE, stdout, on_application_stop)
+ # The origin has to be specified whether it's local or external.
+ assert any("--origin=" in arg for arg in arguments)
+
+ # Extract map-origin arguments.
+ map_parameters, other_parameters = _Split(arguments, _IsMapOrigin)
+ parameters += other_parameters
+ parameters += self._StartHttpServerForOriginMappings(map_parameters,
+ fixed_port)
+
+ if parameters:
+ encodedParameters = json.dumps(parameters)
+ cmd += ['--es', 'encodedParameters', encodedParameters]
+
+ with open(os.devnull, 'w') as devnull:
+ subprocess.Popen(cmd, stdout=devnull).wait()
+
+ def StopShell(self):
+ """
+ Stops the mojo shell.
+ """
+ subprocess.check_call(self._CreateADBCommand(['shell',
+ 'am',
+ 'force-stop',
+ MOJO_SHELL_PACKAGE_NAME]))
+ def CleanLogs(self):
+ """
+ Cleans the logs on the device.
+ """
+ subprocess.check_call(self._CreateADBCommand(['logcat', '-c']))
-def ShowLogs():
- """
- Displays the log for the mojo shell.
+ def ShowLogs(self):
+ """
+ Displays the log for the mojo shell.
- Returns the process responsible for reading the logs.
- """
- logcat = subprocess.Popen([ADB_PATH,
- 'logcat',
- '-s',
- ' '.join(LOGCAT_TAGS)],
- stdout=sys.stdout)
- atexit.register(_ExitIfNeeded, logcat)
- return logcat
+ Returns the process responsible for reading the logs.
+ """
+ logcat = subprocess.Popen(self._CreateADBCommand([
+ 'logcat',
+ '-s',
+ ' '.join(LOGCAT_TAGS)]),
+ stdout=sys.stdout)
+ atexit.register(_ExitIfNeeded, logcat)
+ return logcat
diff --git a/mojo/tools/mopy/config.py b/mojo/tools/mopy/config.py
index 7c8cadd..796a328 100644
--- a/mojo/tools/mopy/config.py
+++ b/mojo/tools/mopy/config.py
@@ -6,8 +6,6 @@
"defines" the schema and provides some wrappers."""
-import json
-import os.path
import platform
import sys
diff --git a/mojo/tools/mopy/dart_apptest.py b/mojo/tools/mopy/dart_apptest.py
new file mode 100644
index 0000000..5e12b17
--- /dev/null
+++ b/mojo/tools/mopy/dart_apptest.py
@@ -0,0 +1,38 @@
+# 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/mojo/tools/mopy/gn.py b/mojo/tools/mopy/gn.py
index 69ddff4..024e00f 100644
--- a/mojo/tools/mopy/gn.py
+++ b/mojo/tools/mopy/gn.py
@@ -132,4 +132,10 @@ def ParseGNConfig(build_dir):
key = result.group(1)
value = result.group(2)
values[key] = ast.literal_eval(TRANSLATIONS.get(value, value))
+
+ # TODO(msw): Mojo's script uses GN's is_debug arg to determine the build dir.
+ # It should probably just use the argument build_dir instead.
+ if not "is_debug" in values:
+ values["is_debug"] = "Debug" in build_dir
+
return values
diff --git a/mojo/tools/mopy/gtest.py b/mojo/tools/mopy/gtest.py
new file mode 100644
index 0000000..1cde8ece
--- /dev/null
+++ b/mojo/tools/mopy/gtest.py
@@ -0,0 +1,132 @@
+# 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
+import os
+import re
+import sys
+
+from mopy import test_util
+from mopy.print_process_error import print_process_error
+
+
+_logger = logging.getLogger()
+
+
+def set_color():
+ """Run gtests with color if we're on a TTY (and we're not being told
+ explicitly what to do)."""
+ if sys.stdout.isatty() and "GTEST_COLOR" not in os.environ:
+ _logger.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_test(config, shell, 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 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)
+ 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.
+
+ 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.
+ """
+ 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)
+ 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)
+ 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})
diff --git a/mojo/tools/mopy/log.py b/mojo/tools/mopy/log.py
new file mode 100644
index 0000000..af57232
--- /dev/null
+++ b/mojo/tools/mopy/log.py
@@ -0,0 +1,29 @@
+# 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/mojo/tools/mopy/paths.py b/mojo/tools/mopy/paths.py
index 49e1c50..60181e8 100644
--- a/mojo/tools/mopy/paths.py
+++ b/mojo/tools/mopy/paths.py
@@ -16,6 +16,8 @@ class Paths(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)
@@ -25,13 +27,11 @@ class Paths(object):
self.build_dir = None
if self.build_dir is not None:
- self.mojo_launcher_path = os.path.join(self.build_dir, "mojo_launcher")
self.mojo_shell_path = os.path.join(self.build_dir, "mojo_shell")
# 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_launcher_path += ".exe"
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,
@@ -40,7 +40,6 @@ class Paths(object):
else:
self.target_mojo_shell_path = self.mojo_shell_path
else:
- self.mojo_launcher_path = None
self.mojo_shell_path = None
self.target_mojo_shell_path = None
diff --git a/mojo/tools/mopy/print_process_error.py b/mojo/tools/mopy/print_process_error.py
new file mode 100644
index 0000000..ec565d1
--- /dev/null
+++ b/mojo/tools/mopy/print_process_error.py
@@ -0,0 +1,22 @@
+# 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/mojo/tools/mopy/test_util.py b/mojo/tools/mopy/test_util.py
new file mode 100644
index 0000000..4a4c128
--- /dev/null
+++ b/mojo/tools/mopy/test_util.py
@@ -0,0 +1,95 @@
+# 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, False)
+ 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