Clone of chromium aad1ce808763f59c7a3753e08f1500a104ecc6fd refs/remotes/origin/HEAD
diff --git a/mojo/tools/BUILD.gn b/mojo/tools/BUILD.gn
new file mode 100644
index 0000000..de38e6c
--- /dev/null
+++ b/mojo/tools/BUILD.gn
@@ -0,0 +1,23 @@
+# 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.
+
+# GYP version: mojo/mojo_base.gyp:mojo_message_generator
+executable("message_generator") {
+ testonly = true
+ output_name = "mojo_message_generator"
+
+ sources = [
+ "message_generator.cc",
+ ]
+
+ deps = [
+ "//base",
+ "//mojo/common",
+ "//mojo/edk/system",
+ "//mojo/environment:chromium",
+ "//mojo/public/cpp/bindings",
+ "//testing/gtest",
+ ]
+}
+
diff --git a/mojo/tools/check_mojom_golden_files.py b/mojo/tools/check_mojom_golden_files.py
new file mode 100755
index 0000000..9af7a86
--- /dev/null
+++ b/mojo/tools/check_mojom_golden_files.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# 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 argparse
+import os.path
+import sys
+from filecmp import dircmp
+from shutil import rmtree
+from tempfile import mkdtemp
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+_mojo_dir = os.path.join(_script_dir, os.pardir)
+_chromium_src_dir = os.path.join(_mojo_dir, os.pardir)
+sys.path.insert(0, os.path.join(_mojo_dir, "public", "tools", "bindings",
+ "pylib"))
+from mojom_tests.support.find_files import FindFiles
+from mojom_tests.support.run_bindings_generator import RunBindingsGenerator
+
+
+def _ProcessDircmpResults(results, verbose=False):
+ """Prints results of directory comparison and returns true if they are
+ identical (note: the "left" directory should be the golden directory)."""
+ rv = not (bool(results.left_only) or bool(results.right_only) or \
+ bool(results.common_funny) or bool(results.funny_files) or \
+ bool(results.diff_files))
+ if verbose:
+ for f in results.left_only:
+ print "%s exists in golden directory but not in current output" % f
+ for f in results.right_only:
+ print "%s exists in current output but not in golden directory" % f
+ for f in results.common_funny + results.funny_files:
+ print "Unable to compare %s between golden directory and current output" \
+ % f
+ for f in results.diff_files:
+ print "%s differs between golden directory and current output" % f
+ for r in results.subdirs.values():
+ # If we're being verbose, check subdirectories even if we know that there
+ # are differences. Note that it's "... and rv" to avoid the short-circuit.
+ if rv or verbose:
+ rv = _ProcessDircmpResults(r, verbose=verbose) and rv
+ return rv
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--generate_golden_files", action="store_true",
+ help=("generate golden files (does not obliterate "
+ "directory"))
+ parser.add_argument("--keep_temp_dir", action="store_true",
+ help="don't delete the temporary directory")
+ parser.add_argument("--verbose", action="store_true",
+ help="spew excess verbiage")
+ parser.add_argument("golden_dir", metavar="GOLDEN_DIR",
+ help="directory with the golden files")
+ args = parser.parse_args()
+
+ if args.generate_golden_files:
+ if os.path.exists(args.golden_dir):
+ print "WARNING: golden directory %s already exists" % args.golden_dir
+ out_dir = args.golden_dir
+ else:
+ if not os.path.exists(args.golden_dir):
+ print "ERROR: golden directory %s does not exist" % args.golden_dir
+ return 1
+ out_dir = mkdtemp()
+ if args.verbose:
+ print "Generating files to %s ..." % out_dir
+
+ mojom_files = FindFiles(_mojo_dir, "*.mojom")
+ for mojom_file in mojom_files:
+ if args.verbose:
+ print " Processing %s ..." % os.path.relpath(mojom_file, _mojo_dir)
+ # TODO(vtl): This may wrong, since the path can be overridden in the .gyp
+ # file.
+ RunBindingsGenerator(out_dir, _mojo_dir, mojom_file,
+ ["-I", os.path.abspath(_chromium_src_dir)])
+
+ if args.generate_golden_files:
+ return 0
+
+ identical = _ProcessDircmpResults(dircmp(args.golden_dir, out_dir, ignore=[]),
+ verbose=args.verbose)
+
+ if args.keep_temp_dir:
+ if args.verbose:
+ print "Not removing %s ..." % out_dir
+ else:
+ if args.verbose:
+ print "Removing %s ..." % out_dir
+ rmtree(out_dir)
+
+ if not identical:
+ print "FAILURE: current output differs from golden files"
+ return 1
+
+ print "SUCCESS: current output identical to golden files"
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/mojo/tools/data/unittests b/mojo/tools/data/unittests
new file mode 100644
index 0000000..0bafba4
--- /dev/null
+++ b/mojo/tools/data/unittests
@@ -0,0 +1,27 @@
+# This file contains a list of Mojo gtest unit tests.
+# Prepend * to indicate that results shouldn't be cached (e.g., if the test has
+# other data dependencies).
+# TODO(vtl): Add a way of specifying data dependencies instead.
+
+# System tests:
+mojo_system_unittests
+
+# Public tests:
+*mojo_public_bindings_unittests
+mojo_public_environment_unittests
+mojo_public_system_unittests
+mojo_public_utility_unittests
+
+# Non-system, non-public tests:
+mojo_application_manager_unittests
+mojo_common_unittests
+mojo_view_manager_lib_unittests
+mojo_view_manager_unittests
+mojo_surfaces_lib_unittests
+
+# JavaScript tests:
+*mojo_apps_js_unittests
+*mojo_js_unittests
+
+# Shell integration tests:
+*mojo_shell_tests
diff --git a/mojo/tools/generate_java_callback_interfaces.py b/mojo/tools/generate_java_callback_interfaces.py
new file mode 100644
index 0000000..257a540
--- /dev/null
+++ b/mojo/tools/generate_java_callback_interfaces.py
@@ -0,0 +1,69 @@
+"""Generate the org.chromium.mojo.bindings.Callbacks interface"""
+
+import argparse
+import sys
+
+CALLBACK_TEMPLATE = ("""
+ /**
+ * A generic %d-argument callback.
+ *
+ * %s
+ */
+ interface Callback%d<%s> {
+ /**
+ * Call the callback.
+ */
+ public void call(%s);
+ }
+""")
+
+INTERFACE_TEMPLATE = (
+"""// 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.
+
+// This file was generated using
+// mojo/tools/generate_java_callback_interfaces.py
+
+package org.chromium.mojo.bindings;
+
+/**
+ * Contains a generic interface for callbacks.
+ */
+public interface Callbacks {
+
+ /**
+ * A generic callback.
+ */
+ interface Callback0 {
+ /**
+ * Call the callback.
+ */
+ public void call();
+ }
+%s
+}""")
+
+def GenerateCallback(nb_args):
+ params = '\n * '.join(
+ ['@param <T%d> the type of argument %d.' % (i+1, i+1)
+ for i in xrange(nb_args)])
+ template_parameters = ', '.join(['T%d' % (i+1) for i in xrange(nb_args)])
+ callback_parameters = ', '.join(['T%d arg%d' % ((i+1), (i+1))
+ for i in xrange(nb_args)])
+ return CALLBACK_TEMPLATE % (nb_args, params, nb_args, template_parameters,
+ callback_parameters)
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Generate org.chromium.mojo.bindings.Callbacks")
+ parser.add_argument("max_args", nargs=1, type=int,
+ help="maximal number of arguments to generate callbacks for")
+ args = parser.parse_args()
+ max_args = args.max_args[0]
+ print INTERFACE_TEMPLATE % ''.join([GenerateCallback(i+1)
+ for i in xrange(max_args)])
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/mojo/tools/message_generator.cc b/mojo/tools/message_generator.cc
new file mode 100644
index 0000000..08b7dcc
--- /dev/null
+++ b/mojo/tools/message_generator.cc
@@ -0,0 +1,63 @@
+// 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.
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "mojo/public/cpp/bindings/lib/message_builder.h"
+#include "mojo/public/cpp/bindings/lib/message_internal.h"
+#include "mojo/public/cpp/bindings/message.h"
+
+// This file is used to generate various files corresponding to mojo
+// messages. The various binding implementations can parse these to verify they
+// correctly decode messages.
+//
+// The output consists of each byte of the message encoded in a hex string with
+// a newline after it.
+namespace mojo {
+namespace {
+
+std::string BinaryToHex(const base::StringPiece& piece) {
+ std::string result("// File generated by mojo_message_generator.\n");;
+ result.reserve(result.size() + (piece.size() * 5));
+ for (size_t i = 0; i < piece.size(); ++i)
+ base::StringAppendF(&result, "0X%.2X\n", static_cast<int>(piece.data()[i]));
+ return result;
+}
+
+void WriteMessageToFile(const Message& message, const base::FilePath& path) {
+ const std::string hex_message(BinaryToHex(
+ base::StringPiece(reinterpret_cast<const char*>(message.data()),
+ message.data_num_bytes())));
+ CHECK_EQ(static_cast<int>(hex_message.size()),
+ base::WriteFile(path, hex_message.data(),
+ static_cast<int>(hex_message.size())));
+}
+
+// Generates a message of type MessageData. The message uses the name 21,
+// with 4 bytes of payload: 0x9, 0x8, 0x7, 0x6.
+void GenerateMessageDataMessage() {
+ internal::MessageBuilder builder(static_cast<uint32_t>(21),
+ static_cast<size_t>(4));
+ char* data = static_cast<char*>(builder.buffer()->Allocate(4));
+ DCHECK(data);
+ data[0] = 9;
+ data[1] = 8;
+ data[2] = 7;
+ data[3] = 6;
+
+ Message message;
+ builder.Finish(&message);
+ WriteMessageToFile(message,
+ base::FilePath(FILE_PATH_LITERAL("message_data")));
+}
+
+} // namespace
+} // namespace mojo
+
+int main(int argc, char** argv) {
+ mojo::GenerateMessageDataMessage();
+ return 0;
+}
diff --git a/mojo/tools/mojob.sh b/mojo/tools/mojob.sh
new file mode 100755
index 0000000..e0804d1
--- /dev/null
+++ b/mojo/tools/mojob.sh
@@ -0,0 +1,238 @@
+#!/bin/bash
+# Copyright 2013 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.
+
+# This a simple script to make building/testing Mojo components easier (on
+# Linux).
+
+# TODO(vtl): Maybe make the test runner smart and not run unchanged test
+# binaries.
+# TODO(vtl) Maybe also provide a way to pass command-line arguments to the test
+# binaries.
+
+do_help() {
+ cat << EOF
+Usage: $(basename "$0") [command|option ...]
+
+command should be one of:
+ build - Build.
+ test - Run unit tests (does not build).
+ perftest - Run perf tests (does not build).
+ pytest - Run Python unit tests.
+ gyp - Run gyp for mojo (does not sync).
+ gypall - Run gyp for all of chromium (does not sync).
+ sync - Sync using gclient (does not run gyp).
+ show-bash-alias - Outputs an appropriate bash alias for mojob. In bash do:
+ \$ eval \`mojo/tools/mojob.sh show-bash-alias\`
+
+option (which will only apply to following commands) should be one of:
+ Build/test options (specified before build/test/perftest):
+ --debug - Build/test in Debug mode.
+ --release - Build/test in Release mode.
+ --debug-and-release - Build/test in both Debug and Release modes (default).
+ Compiler options (specified before gyp):
+ --clang - Use clang (default).
+ --gcc - Use gcc.
+ Component options:
+ --shared Build components as shared libraries (default).
+ --static Build components as static libraries.
+ Use goma:
+ --use-goma - Use goma if \$GOMA_DIR is set or \$HOME/goma exists (default).
+ --no-use-goma - Do not use goma.
+
+Note: It will abort on the first failure (if any).
+EOF
+}
+
+do_build() {
+ echo "Building in out/$1 ..."
+ if [ "$GOMA" = "auto" -a -v GOMA_DIR ]; then
+ ninja -j 1000 -l 100 -C "out/$1" mojo || exit 1
+ else
+ ninja -C "out/$1" mojo || exit 1
+ fi
+}
+
+do_unittests() {
+ echo "Running unit tests in out/$1 ..."
+ mojo/tools/test_runner.py mojo/tools/data/unittests "out/$1" \
+ mojob_test_successes || exit 1
+}
+
+do_perftests() {
+ echo "Running perf tests in out/$1 ..."
+ "out/$1/mojo_public_system_perftests" || exit 1
+}
+
+do_pytests() {
+ python mojo/tools/run_mojo_python_tests.py || exit 1
+}
+
+do_gyp() {
+ local gyp_defines="$(make_gyp_defines)"
+ echo "Running gyp for mojo with GYP_DEFINES=$gyp_defines ..."
+ GYP_DEFINES="$gyp_defines" build/gyp_chromium mojo/mojo.gyp || exit 1
+}
+
+do_gypall() {
+ local gyp_defines="$(make_gyp_defines)"
+ echo "Running gyp for everything with GYP_DEFINES=$gyp_defines ..."
+ GYP_DEFINES="$gyp_defines" build/gyp_chromium || exit 1
+}
+
+do_sync() {
+ # Note: sync only (with hooks, but no gyp-ing).
+ GYP_CHROMIUM_NO_ACTION=1 gclient sync || exit 1
+}
+
+# Valid values: Debug, Release, or Debug_and_Release.
+BUILD_TEST_TYPE=Debug_and_Release
+should_do_Debug() {
+ test "$BUILD_TEST_TYPE" = Debug -o "$BUILD_TEST_TYPE" = Debug_and_Release
+}
+should_do_Release() {
+ test "$BUILD_TEST_TYPE" = Release -o "$BUILD_TEST_TYPE" = Debug_and_Release
+}
+
+# Valid values: clang or gcc.
+COMPILER=clang
+# Valid values: shared or static.
+COMPONENT=shared
+# Valid values: auto or disabled.
+GOMA=auto
+make_gyp_defines() {
+ local options=()
+ # Always include these options.
+ options+=("use_aura=1")
+ case "$COMPILER" in
+ clang)
+ options+=("clang=1")
+ ;;
+ gcc)
+ options+=("clang=0")
+ ;;
+ esac
+ case "$COMPONENT" in
+ shared)
+ options+=("component=shared_library")
+ ;;
+ static)
+ options+=("component=static_library")
+ ;;
+ esac
+ case "$GOMA" in
+ auto)
+ if [ -v GOMA_DIR ]; then
+ options+=("use_goma=1" "gomadir=\"${GOMA_DIR}\"")
+ else
+ options+=("use_goma=0")
+ fi
+ ;;
+ disabled)
+ options+=("use_goma=0")
+ ;;
+ esac
+ echo "${options[*]}"
+}
+
+set_goma_dir_if_necessary() {
+ if [ "$GOMA" = "auto" -a ! -v GOMA_DIR ]; then
+ if [ -d "${HOME}/goma" ]; then
+ GOMA_DIR="${HOME}/goma"
+ fi
+ fi
+}
+
+start_goma_if_necessary() {
+ if [ "$GOMA" = "auto" -a -v GOMA_DIR ]; then
+ "${GOMA_DIR}/goma_ctl.py" ensure_start
+ fi
+}
+
+# We're in src/mojo/tools. We want to get to src.
+cd "$(realpath "$(dirname "$0")")/../.."
+
+if [ $# -eq 0 ]; then
+ do_help
+ exit 0
+fi
+
+for arg in "$@"; do
+ case "$arg" in
+ # Commands -----------------------------------------------------------------
+ help|--help)
+ do_help
+ exit 0
+ ;;
+ build)
+ set_goma_dir_if_necessary
+ start_goma_if_necessary
+ should_do_Debug && do_build Debug
+ should_do_Release && do_build Release
+ ;;
+ test)
+ should_do_Debug && do_unittests Debug
+ should_do_Release && do_unittests Release
+ ;;
+ perftest)
+ should_do_Debug && do_perftests Debug
+ should_do_Release && do_perftests Release
+ ;;
+ pytest)
+ do_pytests
+ ;;
+ gyp)
+ set_goma_dir_if_necessary
+ do_gyp
+ ;;
+ gypall)
+ set_goma_dir_if_necessary
+ do_gypall
+ ;;
+ sync)
+ do_sync
+ ;;
+ show-bash-alias)
+ # You want to type something like:
+ # alias mojob=\
+ # '"$(pwd | sed '"'"'s/\(.*\/src\).*/\1/'"'"')/mojo/tools/mojob.sh"'
+ # This is quoting hell, so we simply escape every non-alphanumeric
+ # character.
+ echo alias\ mojob\=\'\"\$\(pwd\ \|\ sed\ \'\"\'\"\'s\/\\\(\.\*\\\/src\\\)\
+\.\*\/\\1\/\'\"\'\"\'\)\/mojo\/tools\/mojob\.sh\"\'
+ ;;
+ # Options ------------------------------------------------------------------
+ --debug)
+ BUILD_TEST_TYPE=Debug
+ ;;
+ --release)
+ BUILD_TEST_TYPE=Release
+ ;;
+ --debug-and-release)
+ BUILD_TEST_TYPE=Debug_and_Release
+ ;;
+ --clang)
+ COMPILER=clang
+ ;;
+ --gcc)
+ COMPILER=gcc
+ ;;
+ --shared)
+ COMPONENT=shared
+ ;;
+ --static)
+ COMPONENT=static
+ ;;
+ --use-goma)
+ GOMA=auto
+ ;;
+ --no-use-goma)
+ GOMA=disabled
+ ;;
+ *)
+ echo "Unknown command \"${arg}\". Try \"$(basename "$0") help\"."
+ exit 1
+ ;;
+ esac
+done
diff --git a/mojo/tools/mojosh.sh b/mojo/tools/mojosh.sh
new file mode 100755
index 0000000..bb8a2cd
--- /dev/null
+++ b/mojo/tools/mojosh.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# 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.
+
+# This a simple script to make running Mojo shell easier (on Linux).
+
+DIRECTORY="$(dirname "$0")"/../../out/Debug
+PORT=$(($RANDOM % 8192 + 2000))
+
+do_help() {
+ cat << EOF
+Usage: $(basename "$0") [-d DIRECTORY] [-|--] <mojo_shell arguments ...>
+
+DIRECTORY defaults to $DIRECTORY.
+
+Example:
+ $(basename "$0") mojo:mojo_sample_app
+EOF
+}
+
+kill_http_server() {
+ echo "Killing SimpleHTTPServer ..."
+ kill $HTTP_SERVER_PID
+ wait $HTTP_SERVER_PID
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -h|--help)
+ do_help
+ exit 0
+ ;;
+ -d)
+ shift
+ if [ $# -eq 0 ]; then
+ do_help
+ exit 1
+ fi
+ DIRECTORY="$1"
+ ;;
+ --show-bash-alias)
+ echo alias\ mojosh\=\'\"\$\(pwd\ \|\ sed\ \'\"\'\"\'s\/\\\(\.\*\\\/src\\\
+\)\.\*\/\\1\/\'\"\'\"\'\)\/mojo\/tools\/mojosh\.sh\"\'
+ exit 0
+ ;;
+ # Separate arguments to mojo_shell (e.g., in case you want to pass it -d).
+ -|--)
+ shift
+ break
+ ;;
+ *)
+ break
+ ;;
+ esac
+ shift
+done
+
+echo "Base directory: $DIRECTORY"
+
+echo "Running SimpleHTTPServer in directory $DIRECTORY/lib on port $PORT"
+cd $DIRECTORY/lib || exit 1
+python -m SimpleHTTPServer $PORT &
+# Kill the HTTP server on exit (even if the user kills everything using ^C).
+HTTP_SERVER_PID=$!
+trap kill_http_server EXIT
+cd ..
+
+echo "Running:"
+echo "./mojo_shell --origin=http://127.0.0.1:$PORT --disable-cache $*"
+./mojo_shell --origin=http://127.0.0.1:$PORT --disable-cache $*
diff --git a/mojo/tools/package_manager/BUILD.gn b/mojo/tools/package_manager/BUILD.gn
new file mode 100644
index 0000000..76b5771
--- /dev/null
+++ b/mojo/tools/package_manager/BUILD.gn
@@ -0,0 +1,29 @@
+# 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.
+
+shared_library("package_manager") {
+ output_name = "mojo_package_manager"
+
+ sources = [
+ "manifest.cc",
+ "manifest.h",
+ "package_fetcher.cc",
+ "package_fetcher.h",
+ "package_manager.cc",
+ "package_manager_application.cc",
+ "package_manager_application.h",
+ "unpacker.cc",
+ "unpacker.h",
+ ]
+
+ deps = [
+ "//base",
+ "//mojo/application",
+ "//mojo/public/c/system:for_shared_library",
+ "//mojo/public/cpp/utility",
+ "//mojo/services/public/interfaces/network",
+ "//third_party/zlib:zip",
+ "//url",
+ ]
+}
diff --git a/mojo/tools/package_manager/DEPS b/mojo/tools/package_manager/DEPS
new file mode 100644
index 0000000..784b7fb
--- /dev/null
+++ b/mojo/tools/package_manager/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+ '+third_party/zlib',
+]
diff --git a/mojo/tools/package_manager/manifest.cc b/mojo/tools/package_manager/manifest.cc
new file mode 100644
index 0000000..5a0e346
--- /dev/null
+++ b/mojo/tools/package_manager/manifest.cc
@@ -0,0 +1,83 @@
+// 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.
+
+#include "mojo/tools/package_manager/manifest.h"
+
+#include "base/files/file_util.h"
+#include "base/json/json_reader.h"
+#include "base/values.h"
+#include "url/gurl.h"
+
+namespace mojo {
+
+Manifest::Manifest() {
+}
+
+Manifest::~Manifest() {
+}
+
+bool Manifest::Parse(const std::string& str, std::string* err_msg) {
+ int err_code = base::JSONReader::JSON_NO_ERROR;
+ scoped_ptr<base::Value> root(base::JSONReader::ReadAndReturnError(
+ str,
+ base::JSON_ALLOW_TRAILING_COMMAS,
+ &err_code, err_msg));
+ if (err_code != base::JSONReader::JSON_NO_ERROR)
+ return false;
+
+ const base::DictionaryValue* root_dict;
+ if (!root->GetAsDictionary(&root_dict)) {
+ *err_msg = "Manifest is not a dictionary.";
+ return false;
+ }
+
+ if (!PopulateDeps(root_dict, err_msg))
+ return false;
+
+ return true;
+}
+
+bool Manifest::ParseFromFile(const base::FilePath& file_name,
+ std::string* err_msg) {
+ std::string data;
+ if (!base::ReadFileToString(file_name, &data)) {
+ *err_msg = "Couldn't read manifest file " + file_name.AsUTF8Unsafe();
+ return false;
+ }
+ return Parse(data, err_msg);
+}
+
+bool Manifest::PopulateDeps(const base::DictionaryValue* root,
+ std::string* err_msg) {
+ const base::Value* deps_value;
+ if (!root->Get("deps", &deps_value))
+ return true; // No deps, that's OK.
+
+ const base::ListValue* deps;
+ if (!deps_value->GetAsList(&deps)) {
+ *err_msg = "Deps is not a list. Should be \"deps\": [ \"...\", \"...\" ]";
+ return false;
+ }
+
+ deps_.reserve(deps->GetSize());
+ for (size_t i = 0; i < deps->GetSize(); i++) {
+ std::string cur_dep;
+ if (!deps->GetString(i, &cur_dep)) {
+ *err_msg = "Dependency list item wasn't a string.";
+ return false;
+ }
+
+ GURL cur_url(cur_dep);
+ if (!cur_url.is_valid()) {
+ *err_msg = "Dependency entry isn't a valid URL: " + cur_dep;
+ return false;
+ }
+
+ deps_.push_back(cur_url);
+ }
+
+ return true;
+}
+
+} // namespace mojo
diff --git a/mojo/tools/package_manager/manifest.h b/mojo/tools/package_manager/manifest.h
new file mode 100644
index 0000000..f87040d
--- /dev/null
+++ b/mojo/tools/package_manager/manifest.h
@@ -0,0 +1,46 @@
+// 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.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
+
+#include <string>
+#include <vector>
+
+#include "mojo/public/cpp/system/macros.h"
+
+class GURL;
+
+namespace base {
+class DictionaryValue;
+class FilePath;
+}
+
+namespace mojo {
+
+class Manifest {
+ public:
+ Manifest();
+ ~Manifest();
+
+ // Parses the manifest from raw data. Returns true on success. On failure,
+ // populates the "err_msg" string with an error.
+ bool Parse(const std::string& str, std::string* err_msg);
+
+ // Like Parse but reads the data from a file.
+ bool ParseFromFile(const base::FilePath& file_name, std::string* err_msg);
+
+ const std::vector<GURL>& deps() const { return deps_; }
+
+ private:
+ bool PopulateDeps(const base::DictionaryValue* root, std::string* err_msg);
+
+ std::vector<GURL> deps_;
+
+ MOJO_DISALLOW_COPY_AND_ASSIGN(Manifest);
+};
+
+} // namespace mojo
+
+#endif // MOJO_TOOLS_PACKAGE_MANAGER_MANIFEST_H_
diff --git a/mojo/tools/package_manager/package_fetcher.cc b/mojo/tools/package_manager/package_fetcher.cc
new file mode 100644
index 0000000..b5a47e5
--- /dev/null
+++ b/mojo/tools/package_manager/package_fetcher.cc
@@ -0,0 +1,96 @@
+// 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.
+
+#include "mojo/tools/package_manager/package_fetcher.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "mojo/services/public/interfaces/network/url_loader.mojom.h"
+
+namespace mojo {
+
+PackageFetcher::PackageFetcher(NetworkService* network_service,
+ PackageFetcherDelegate* delegate,
+ const GURL& url)
+ : delegate_(delegate),
+ url_(url) {
+ network_service->CreateURLLoader(Get(&loader_));
+
+ URLRequestPtr request(URLRequest::New());
+ request->url = url.spec();
+ request->auto_follow_redirects = true;
+
+ loader_->Start(request.Pass(),
+ base::Bind(&PackageFetcher::OnReceivedResponse,
+ base::Unretained(this)));
+}
+
+PackageFetcher::~PackageFetcher() {
+}
+
+void PackageFetcher::OnReceivedResponse(URLResponsePtr response) {
+ if (response->error) {
+ printf("Got error %d (%s) for %s\n",
+ response->error->code,
+ response->error->description.get().c_str(),
+ url_.spec().c_str());
+ delegate_->FetchFailed(url_);
+ return;
+ }
+
+ if (!base::CreateTemporaryFile(&output_file_name_)) {
+ delegate_->FetchFailed(url_);
+ return;
+ }
+ output_file_.Initialize(output_file_name_,
+ base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+ if (!output_file_.IsValid()) {
+ base::DeleteFile(output_file_name_, false);
+ delegate_->FetchFailed(url_);
+ // Danger: may be deleted now.
+ return;
+ }
+
+ body_ = response->body.Pass();
+ ReadData(MOJO_RESULT_OK);
+ // Danger: may be deleted now.
+}
+
+void PackageFetcher::ReadData(MojoResult) {
+ char buf[4096];
+ uint32_t num_bytes = sizeof(buf);
+ MojoResult result = ReadDataRaw(body_.get(), buf, &num_bytes,
+ MOJO_READ_DATA_FLAG_NONE);
+ if (result == MOJO_RESULT_SHOULD_WAIT) {
+ WaitToReadMore();
+ } else if (result == MOJO_RESULT_OK) {
+ if (output_file_.WriteAtCurrentPos(buf, num_bytes) !=
+ static_cast<int>(num_bytes)) {
+ // Clean up the output file.
+ output_file_.Close();
+ base::DeleteFile(output_file_name_, false);
+
+ delegate_->FetchFailed(url_);
+ // Danger: may be deleted now.
+ return;
+ }
+ WaitToReadMore();
+ } else if (result == MOJO_RESULT_FAILED_PRECONDITION) {
+ // Done.
+ output_file_.Close();
+ delegate_->FetchSucceeded(url_, output_file_name_);
+ // Danger: may be deleted now.
+ }
+}
+
+void PackageFetcher::WaitToReadMore() {
+ handle_watcher_.Start(
+ body_.get(),
+ MOJO_HANDLE_SIGNAL_READABLE,
+ MOJO_DEADLINE_INDEFINITE,
+ base::Bind(&PackageFetcher::ReadData, base::Unretained(this)));
+}
+
+} // namespace mojo
diff --git a/mojo/tools/package_manager/package_fetcher.h b/mojo/tools/package_manager/package_fetcher.h
new file mode 100644
index 0000000..3cbd7a7
--- /dev/null
+++ b/mojo/tools/package_manager/package_fetcher.h
@@ -0,0 +1,63 @@
+// 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.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
+
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "mojo/common/handle_watcher.h"
+#include "mojo/public/cpp/system/macros.h"
+#include "mojo/services/public/interfaces/network/network_service.mojom.h"
+#include "mojo/services/public/interfaces/network/url_loader.mojom.h"
+#include "url/gurl.h"
+
+namespace base {
+class FilePath;
+} // namespace base
+
+namespace mojo {
+
+class PackageFetcherDelegate;
+
+class PackageFetcher {
+ public:
+ PackageFetcher(NetworkService* network_service,
+ PackageFetcherDelegate* delegate,
+ const GURL& url);
+ virtual ~PackageFetcher();
+
+ private:
+ void OnReceivedResponse(URLResponsePtr response);
+
+ void ReadData(MojoResult);
+ void WaitToReadMore();
+
+ PackageFetcherDelegate* delegate_;
+
+ // URL of the package.
+ GURL url_;
+
+ URLLoaderPtr loader_;
+
+ // Valid once file has started streaming.
+ ScopedDataPipeConsumerHandle body_;
+ common::HandleWatcher handle_watcher_;
+
+ base::FilePath output_file_name_;
+ base::File output_file_;
+
+ MOJO_DISALLOW_COPY_AND_ASSIGN(PackageFetcher);
+};
+
+class PackageFetcherDelegate {
+ public:
+ virtual void FetchSucceeded(const GURL& url, const base::FilePath& name) = 0;
+
+ virtual void FetchFailed(const GURL& url) = 0;
+};
+
+} // namespace mojo
+
+#endif // MOJO_TOOLS_PACKAGE_MANAGER_FETCHER_H_
diff --git a/mojo/tools/package_manager/package_manager.cc b/mojo/tools/package_manager/package_manager.cc
new file mode 100644
index 0000000..b091f6d
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager.cc
@@ -0,0 +1,12 @@
+// 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.
+
+#include "mojo/application/application_runner_chromium.h"
+#include "mojo/public/c/system/main.h"
+#include "mojo/tools/package_manager/package_manager_application.h"
+
+MojoResult MojoMain(MojoHandle shell_handle) {
+ mojo::ApplicationRunnerChromium runner(new mojo::PackageManagerApplication);
+ return runner.Run(shell_handle);
+}
diff --git a/mojo/tools/package_manager/package_manager_application.cc b/mojo/tools/package_manager/package_manager_application.cc
new file mode 100644
index 0000000..ae4d4a4
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager_application.cc
@@ -0,0 +1,113 @@
+// 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.
+
+#include "mojo/tools/package_manager/package_manager_application.h"
+
+#include "base/files/file_util.h"
+#include "mojo/tools/package_manager/manifest.h"
+#include "mojo/tools/package_manager/unpacker.h"
+#include "base/message_loop/message_loop.h"
+#include "base/stl_util.h"
+#include "mojo/public/cpp/application/application_impl.h"
+
+namespace mojo {
+
+namespace {
+
+const base::FilePath::CharType kManifestFileName[] =
+ FILE_PATH_LITERAL("manifest.json");
+
+} // namespace
+
+PackageManagerApplication::PendingLoad::PendingLoad() {
+}
+
+PackageManagerApplication::PendingLoad::~PendingLoad() {
+}
+
+PackageManagerApplication::PackageManagerApplication() {
+}
+
+PackageManagerApplication::~PackageManagerApplication() {
+ STLDeleteContainerPairSecondPointers(pending_.begin(), pending_.end());
+}
+
+void PackageManagerApplication::Initialize(ApplicationImpl* app) {
+ app->ConnectToService("mojo:mojo_network_service", &network_service_);
+
+ printf("Enter URL> ");
+ char buf[1024];
+ if (scanf("%1023s", buf) != 1) {
+ printf("No input, exiting\n");
+ base::MessageLoop::current()->Quit();
+ return;
+ }
+ GURL url(buf);
+ if (!url.is_valid()) {
+ printf("Invalid URL\n");
+ base::MessageLoop::current()->Quit();
+ return;
+ }
+
+ StartLoad(url);
+}
+
+void PackageManagerApplication::StartLoad(const GURL& url) {
+ if (completed_.find(url) != completed_.end() ||
+ pending_.find(url) != pending_.end())
+ return; // Already loaded or are loading this one.
+
+ PendingLoad* load = new PendingLoad;
+ load->fetcher.reset(new mojo::PackageFetcher(
+ network_service_.get(), this, url));
+ pending_[url] = load;
+}
+
+void PackageManagerApplication::LoadDeps(const Manifest& manifest) {
+ for (size_t i = 0; i < manifest.deps().size(); i++)
+ StartLoad(manifest.deps()[i]);
+}
+
+void PackageManagerApplication::PendingLoadComplete(const GURL& url) {
+ pending_.erase(pending_.find(url));
+ completed_.insert(url);
+ if (pending_.empty())
+ base::MessageLoop::current()->Quit();
+}
+
+void PackageManagerApplication::FetchSucceeded(
+ const GURL& url,
+ const base::FilePath& name) {
+ Unpacker unpacker;
+ if (!unpacker.Unpack(name)) {
+ base::DeleteFile(name, false);
+ printf("Failed to unpack\n");
+ PendingLoadComplete(url);
+ return;
+ }
+ // The downloaded .zip file is no longer necessary.
+ base::DeleteFile(name, false);
+
+ // Load the manifest.
+ base::FilePath manifest_path = unpacker.dir().Append(kManifestFileName);
+ Manifest manifest;
+ std::string err_msg;
+ if (!manifest.ParseFromFile(manifest_path, &err_msg)) {
+ printf("%s\n", err_msg.c_str());
+ PendingLoadComplete(url);
+ return;
+ }
+
+ // Enqueue loads for any deps.
+ LoadDeps(manifest);
+
+ printf("Loaded %s\n", url.spec().c_str());
+ PendingLoadComplete(url);
+}
+
+void PackageManagerApplication::FetchFailed(const GURL& url) {
+ PendingLoadComplete(url);
+}
+
+} // namespace mojo
diff --git a/mojo/tools/package_manager/package_manager_application.h b/mojo/tools/package_manager/package_manager_application.h
new file mode 100644
index 0000000..87d4fcd
--- /dev/null
+++ b/mojo/tools/package_manager/package_manager_application.h
@@ -0,0 +1,63 @@
+// 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.
+
+#ifndef MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H_
+#define MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H_
+
+#include <map>
+#include <set>
+
+#include "mojo/public/cpp/application/application_delegate.h"
+#include "mojo/public/cpp/application/interface_factory.h"
+#include "mojo/public/cpp/system/macros.h"
+#include "mojo/services/public/interfaces/network/network_service.mojom.h"
+#include "mojo/tools/package_manager/package_fetcher.h"
+
+namespace mojo {
+
+class Manifest;
+
+class PackageManagerApplication
+ : public ApplicationDelegate,
+ public PackageFetcherDelegate {
+ public:
+ PackageManagerApplication();
+ virtual ~PackageManagerApplication();
+
+ private:
+ struct PendingLoad {
+ PendingLoad();
+ ~PendingLoad();
+
+ scoped_ptr<PackageFetcher> fetcher;
+ };
+ typedef std::map<GURL, PendingLoad*> PendingLoadMap;
+
+ void StartLoad(const GURL& url);
+
+ void LoadDeps(const Manifest& manifest);
+
+ // Deletes the pending load entry for the given URL and possibly exits the
+ // message loop if all loads are done.
+ void PendingLoadComplete(const GURL& url);
+
+ // ApplicationDelegate implementation.
+ virtual void Initialize(ApplicationImpl* app) override;
+
+ // PackageFetcher.
+ virtual void FetchSucceeded(const GURL& url,
+ const base::FilePath& name) override;
+ virtual void FetchFailed(const GURL& url) override;
+
+ mojo::NetworkServicePtr network_service_;
+
+ PendingLoadMap pending_; // Owning pointers.
+ std::set<GURL> completed_;
+
+ MOJO_DISALLOW_COPY_AND_ASSIGN(PackageManagerApplication);
+};
+
+} // namespace mojo
+
+#endif // MOJO_PACKAGE_MANAGER_PACKAGE_MANAGER_APPLICATION_H
diff --git a/mojo/tools/package_manager/unpacker.cc b/mojo/tools/package_manager/unpacker.cc
new file mode 100644
index 0000000..8ed34d5
--- /dev/null
+++ b/mojo/tools/package_manager/unpacker.cc
@@ -0,0 +1,35 @@
+// 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.
+
+#include "mojo/tools/package_manager/unpacker.h"
+
+#include "base/files/file_util.h"
+#include "base/logging.h"
+#include "third_party/zlib/google/zip.h"
+
+namespace mojo {
+
+Unpacker::Unpacker() {
+}
+
+Unpacker::~Unpacker() {
+ if (!dir_.empty())
+ DeleteFile(dir_, true);
+}
+
+bool Unpacker::Unpack(const base::FilePath& zip_file) {
+ DCHECK(zip_file_.empty());
+ zip_file_ = zip_file;
+
+ DCHECK(dir_.empty());
+ if (!CreateNewTempDirectory(base::FilePath::StringType(), &dir_))
+ return false;
+ if (!zip::Unzip(zip_file, dir_)) {
+ dir_ = base::FilePath();
+ return false;
+ }
+ return true;
+}
+
+} // namespace mojo
diff --git a/mojo/tools/package_manager/unpacker.h b/mojo/tools/package_manager/unpacker.h
new file mode 100644
index 0000000..2f7acc5
--- /dev/null
+++ b/mojo/tools/package_manager/unpacker.h
@@ -0,0 +1,40 @@
+// 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.
+
+#ifndef MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
+#define MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
+
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "mojo/public/cpp/system/macros.h"
+
+namespace mojo {
+
+// Unzips a package into a temporary folder. The temporary folder will be
+// deleted when the object is destroyed.
+//
+// In the future, this class would probably manage doing the unzip operation on
+// a background thread.
+class Unpacker {
+ public:
+ Unpacker();
+ ~Unpacker();
+
+ // Actually does the unpacking, returns true on success.
+ bool Unpack(const base::FilePath& zip_file);
+
+ // The root directory where the package has been unpacked.
+ const base::FilePath& dir() const { return dir_; }
+
+ private:
+ base::FilePath zip_file_;
+
+ base::FilePath dir_;
+
+ MOJO_DISALLOW_COPY_AND_ASSIGN(Unpacker);
+};
+
+} // namespace mojo
+
+#endif // MOJO_TOOLS_PACKAGE_MANAGER_UNPACKER_H_
diff --git a/mojo/tools/pylib/mojo_python_tests_runner.py b/mojo/tools/pylib/mojo_python_tests_runner.py
new file mode 100644
index 0000000..e63a2a4
--- /dev/null
+++ b/mojo/tools/pylib/mojo_python_tests_runner.py
@@ -0,0 +1,147 @@
+# 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 argparse
+import json
+import os
+import sys
+import time
+import unittest
+
+
+class MojoPythonTestRunner(object):
+ """Helper class to run python tests on the bots."""
+
+ def __init__(self, test_dir):
+ self._test_dir = test_dir
+
+ def run(self):
+ parser = argparse.ArgumentParser()
+ parser.usage = 'run_mojo_python_tests.py [options] [tests...]'
+ parser.add_argument('-v', '--verbose', action='count', default=0)
+ parser.add_argument('--metadata', action='append', default=[],
+ help=('optional key=value metadata that will be stored '
+ 'in the results files (can be used for revision '
+ 'numbers, etc.)'))
+ parser.add_argument('--write-full-results-to', metavar='FILENAME',
+ action='store',
+ help='path to write the list of full results to.')
+ parser.add_argument('tests', nargs='*')
+
+ self.add_custom_commandline_options(parser)
+ args = parser.parse_args()
+ self.apply_customization(args)
+
+ bad_metadata = False
+ for val in args.metadata:
+ if '=' not in val:
+ print >> sys.stderr, ('Error: malformed metadata "%s"' % val)
+ bad_metadata = True
+ if bad_metadata:
+ print >> sys.stderr
+ parser.print_help()
+ return 2
+
+ chromium_src_dir = os.path.join(os.path.dirname(__file__),
+ os.pardir,
+ os.pardir,
+ os.pardir)
+
+ loader = unittest.loader.TestLoader()
+ print "Running Python unit tests under %s..." % self._test_dir
+
+ pylib_dir = os.path.abspath(os.path.join(chromium_src_dir, self._test_dir))
+ if args.tests:
+ if pylib_dir not in sys.path:
+ sys.path.append(pylib_dir)
+ suite = unittest.TestSuite()
+ for test_name in args.tests:
+ suite.addTests(loader.loadTestsFromName(test_name))
+ else:
+ suite = loader.discover(pylib_dir, pattern='*_unittest.py')
+
+ runner = unittest.runner.TextTestRunner(verbosity=(args.verbose + 1))
+ result = runner.run(suite)
+
+ full_results = _FullResults(suite, result, args.metadata)
+ if args.write_full_results_to:
+ with open(args.write_full_results_to, 'w') as fp:
+ json.dump(full_results, fp, indent=2)
+ fp.write("\n")
+
+ return 0 if result.wasSuccessful() else 1
+
+ def add_custom_commandline_options(self, parser):
+ """Allow to add custom option to the runner script."""
+ pass
+
+ def apply_customization(self, args):
+ """Allow to apply any customization to the runner."""
+ pass
+
+
+TEST_SEPARATOR = '.'
+
+
+def _FullResults(suite, result, metadata):
+ """Convert the unittest results to the Chromium JSON test result format.
+
+ This matches run-webkit-tests (the layout tests) and the flakiness dashboard.
+ """
+
+ full_results = {}
+ full_results['interrupted'] = False
+ full_results['path_delimiter'] = TEST_SEPARATOR
+ full_results['version'] = 3
+ full_results['seconds_since_epoch'] = time.time()
+ for md in metadata:
+ key, val = md.split('=', 1)
+ full_results[key] = val
+
+ all_test_names = _AllTestNames(suite)
+ failed_test_names = _FailedTestNames(result)
+
+ full_results['num_failures_by_type'] = {
+ 'FAIL': len(failed_test_names),
+ 'PASS': len(all_test_names) - len(failed_test_names),
+ }
+
+ full_results['tests'] = {}
+
+ for test_name in all_test_names:
+ value = {}
+ value['expected'] = 'PASS'
+ if test_name in failed_test_names:
+ value['actual'] = 'FAIL'
+ value['is_unexpected'] = True
+ else:
+ value['actual'] = 'PASS'
+ _AddPathToTrie(full_results['tests'], test_name, value)
+
+ return full_results
+
+
+def _AllTestNames(suite):
+ test_names = []
+ # _tests is protected pylint: disable=W0212
+ for test in suite._tests:
+ if isinstance(test, unittest.suite.TestSuite):
+ test_names.extend(_AllTestNames(test))
+ else:
+ test_names.append(test.id())
+ return test_names
+
+
+def _FailedTestNames(result):
+ return set(test.id() for test, _ in result.failures + result.errors)
+
+
+def _AddPathToTrie(trie, path, value):
+ if TEST_SEPARATOR not in path:
+ trie[path] = value
+ return
+ directory, rest = path.split(TEST_SEPARATOR, 1)
+ if directory not in trie:
+ trie[directory] = {}
+ _AddPathToTrie(trie[directory], rest, value)
diff --git a/mojo/tools/pylib/transitive_hash.py b/mojo/tools/pylib/transitive_hash.py
new file mode 100644
index 0000000..93e8dc4
--- /dev/null
+++ b/mojo/tools/pylib/transitive_hash.py
@@ -0,0 +1,89 @@
+# 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 subprocess
+import sys
+
+from hashlib import sha256
+from os.path import basename, realpath
+
+_logging = logging.getLogger()
+
+# Based on/taken from
+# http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/
+# (with cosmetic changes).
+def _memoize(f):
+ """Memoization decorator for a function taking a single argument."""
+ class Memoize(dict):
+ def __missing__(self, key):
+ rv = self[key] = f(key)
+ return rv
+ return Memoize().__getitem__
+
+@_memoize
+def _file_hash(filename):
+ """Returns a string representing the hash of the given file."""
+ _logging.debug("Hashing %s ...", filename)
+ rv = subprocess.check_output(['sha256sum', '-b', filename]).split(None, 1)[0]
+ _logging.debug(" => %s", rv)
+ return rv
+
+@_memoize
+def _get_dependencies(filename):
+ """Returns a list of filenames for files that the given file depends on."""
+ _logging.debug("Getting dependencies for %s ...", filename)
+ lines = subprocess.check_output(['ldd', filename]).splitlines()
+ rv = []
+ for line in lines:
+ i = line.find('/')
+ if i < 0:
+ _logging.debug(" => no file found in line: %s", line)
+ continue
+ rv.append(line[i:].split(None, 1)[0])
+ _logging.debug(" => %s", rv)
+ return rv
+
+def transitive_hash(filename):
+ """Returns a string that represents the "transitive" hash of the given
+ file. The transitive hash is a hash of the file and all the shared libraries
+ on which it depends (done in an order-independent way)."""
+ hashes = set()
+ to_hash = [filename]
+ while to_hash:
+ current_filename = realpath(to_hash.pop())
+ current_hash = _file_hash(current_filename)
+ if current_hash in hashes:
+ _logging.debug("Already seen %s (%s) ...", current_filename, current_hash)
+ continue
+ _logging.debug("Haven't seen %s (%s) ...", current_filename, current_hash)
+ hashes.add(current_hash)
+ to_hash.extend(_get_dependencies(current_filename))
+ return sha256('|'.join(sorted(hashes))).hexdigest()
+
+def main(argv):
+ logging.basicConfig()
+ # Uncomment to debug:
+ # _logging.setLevel(logging.DEBUG)
+
+ if len(argv) < 2:
+ print """\
+Usage: %s [file] ...
+
+Prints the \"transitive\" hash of each (executable) file. The transitive
+hash is a hash of the file and all the shared libraries on which it
+depends (done in an order-independent way).""" % basename(argv[0])
+ return 0
+
+ rv = 0
+ for filename in argv[1:]:
+ try:
+ print transitive_hash(filename), filename
+ except:
+ print "ERROR", filename
+ rv = 1
+ return rv
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/mojo/tools/run_mojo_python_bindings_tests.py b/mojo/tools/run_mojo_python_bindings_tests.py
new file mode 100755
index 0000000..916c68d
--- /dev/null
+++ b/mojo/tools/run_mojo_python_bindings_tests.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# 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 os
+import sys
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from mojo_python_tests_runner import MojoPythonTestRunner
+
+
+class PythonBindingsTestRunner(MojoPythonTestRunner):
+
+ def add_custom_commandline_options(self, parser):
+ parser.add_argument('--build-dir', action='store',
+ help='path to the build output directory')
+
+ def apply_customization(self, args):
+ if args.build_dir:
+ python_build_dir = os.path.join(args.build_dir, 'python')
+ if python_build_dir not in sys.path:
+ sys.path.append(python_build_dir)
+ python_gen_dir = os.path.join(
+ args.build_dir,
+ 'gen', 'mojo', 'public', 'interfaces', 'bindings', 'tests')
+ if python_gen_dir not in sys.path:
+ sys.path.append(python_gen_dir)
+
+
+def main():
+ runner = PythonBindingsTestRunner(os.path.join('mojo', 'python', 'tests'))
+ sys.exit(runner.run())
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/mojo/tools/run_mojo_python_tests.py b/mojo/tools/run_mojo_python_tests.py
new file mode 100755
index 0000000..d83ca54
--- /dev/null
+++ b/mojo/tools/run_mojo_python_tests.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# 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 os
+import sys
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from mojo_python_tests_runner import MojoPythonTestRunner
+
+
+def main():
+ runner = MojoPythonTestRunner(os.path.join('mojo', 'public', 'tools',
+ 'bindings', 'pylib'))
+ sys.exit(runner.run())
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/mojo/tools/test_runner.py b/mojo/tools/test_runner.py
new file mode 100755
index 0000000..b0bb4d9
--- /dev/null
+++ b/mojo/tools/test_runner.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# 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 "smart" test runner for gtest unit tests (that caches successes)."""
+
+import logging
+import os
+import subprocess
+import sys
+
+_logging = logging.getLogger()
+
+_script_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.join(_script_dir, "pylib"))
+
+from transitive_hash import transitive_hash
+
+def main(argv):
+ logging.basicConfig()
+ # Uncomment to debug:
+ # _logging.setLevel(logging.DEBUG)
+
+ if len(argv) < 3 or len(argv) > 4:
+ print "Usage: %s gtest_list_file root_dir [successes_cache_file]" % \
+ os.path.basename(argv[0])
+ return 0 if len(argv) < 2 else 1
+
+ _logging.debug("Test list file: %s", argv[1])
+ with open(argv[1], 'rb') as f:
+ gtest_list = [y for y in [x.strip() for x in f.readlines()] \
+ if y and y[0] != '#']
+ _logging.debug("Test list: %s" % gtest_list)
+
+ print "Running tests in directory: %s" % argv[2]
+ os.chdir(argv[2])
+
+ if len(argv) == 4 and argv[3]:
+ successes_cache_filename = argv[3]
+ print "Successes cache file: %s" % successes_cache_filename
+ else:
+ successes_cache_filename = None
+ print "No successes cache file (will run all tests unconditionally)"
+
+ if successes_cache_filename:
+ # This file simply contains a list of transitive hashes of tests that
+ # succeeded.
+ try:
+ _logging.debug("Trying to read successes cache file: %s",
+ successes_cache_filename)
+ with open(argv[3], 'rb') as f:
+ successes = set([x.strip() for x in f.readlines()])
+ _logging.debug("Successes: %s", successes)
+ except:
+ # Just assume that it didn't exist, or whatever.
+ print "Failed to read successes cache file %s (will create)" % argv[3]
+ successes = set()
+
+ # 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'
+
+ # TODO(vtl): We may not close this file on failure.
+ successes_cache_file = open(successes_cache_filename, 'ab') \
+ if successes_cache_filename else None
+ for gtest in gtest_list:
+ if gtest[0] == '*':
+ gtest = gtest[1:]
+ _logging.debug("%s is marked as non-cacheable" % gtest)
+ cacheable = False
+ else:
+ cacheable = True
+
+ if successes_cache_file and cacheable:
+ _logging.debug("Getting transitive hash for %s ... " % gtest)
+ try:
+ gtest_hash = transitive_hash(gtest)
+ except:
+ print "Failed to get transitive hash for %s" % gtest
+ return 1
+ _logging.debug(" Transitive hash: %s" % gtest_hash)
+
+ if gtest_hash in successes:
+ print "Skipping %s (previously succeeded)" % gtest
+ continue
+
+ print "Running %s...." % gtest,
+ sys.stdout.flush()
+ try:
+ subprocess.check_output(["./" + gtest], stderr=subprocess.STDOUT)
+ print "Succeeded"
+ # Record success.
+ if successes_cache_filename and cacheable:
+ successes.add(gtest_hash)
+ successes_cache_file.write(gtest_hash + '\n')
+ successes_cache_file.flush()
+ except subprocess.CalledProcessError as e:
+ print "Failed with exit code %d and output:" % e.returncode
+ print 72 * '-'
+ print e.output
+ print 72 * '-'
+ return 1
+ except OSError as e:
+ print " Failed to start test"
+ return 1
+ print "All tests succeeded"
+ if successes_cache_file:
+ successes_cache_file.close()
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))