Import Cobalt 20.master.0.215766
diff --git a/src/third_party/web_platform_tests/tools/ci/__init__.py b/src/third_party/web_platform_tests/tools/ci/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/__init__.py
diff --git a/src/third_party/web_platform_tests/tools/ci/before_install.sh b/src/third_party/web_platform_tests/tools/ci/before_install.sh
new file mode 100755
index 0000000..ff2609e
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/before_install.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+
+if [[ $(./wpt test-jobs --includes $JOB; echo $?) -eq 0 ]]; then
+    export RUN_JOB=1
+    git submodule update --init --recursive 1>&2
+    export DISPLAY=:99.0
+    sh -e /etc/init.d/xvfb start 1>&2
+else
+    export RUN_JOB=0
+fi
diff --git a/src/third_party/web_platform_tests/tools/ci/check_stability.py b/src/third_party/web_platform_tests/tools/ci/check_stability.py
new file mode 100644
index 0000000..cf3e10d
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/check_stability.py
@@ -0,0 +1,385 @@
+from __future__ import print_function
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+from ConfigParser import SafeConfigParser
+
+import requests
+
+wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+sys.path.insert(0, wpt_root)
+
+from tools.wpt import testfiles
+from tools.wpt.testfiles import get_git_cmd
+from tools.wpt.virtualenv import Virtualenv
+from tools.wpt.utils import Kwargs
+from tools.wpt.run import create_parser, setup_wptrunner
+from tools.wpt import markdown
+from tools import localpaths
+
+logger = None
+run, write_inconsistent, write_results = None, None, None
+wptrunner = None
+
+def setup_logging():
+    """Set up basic debug logger."""
+    handler = logging.StreamHandler(sys.stdout)
+    formatter = logging.Formatter(logging.BASIC_FORMAT, None)
+    handler.setFormatter(formatter)
+    logger.addHandler(handler)
+    logger.setLevel(logging.DEBUG)
+
+
+def do_delayed_imports():
+    global run, write_inconsistent, write_results, wptrunner
+    from tools.wpt.stability import run, write_inconsistent, write_results
+    from wptrunner import wptrunner
+
+
+class TravisFold(object):
+    """Context for TravisCI folding mechanism. Subclasses object.
+
+    See: https://blog.travis-ci.com/2013-05-22-improving-build-visibility-log-folds/
+    """
+
+    def __init__(self, name):
+        """Register TravisCI folding section name."""
+        self.name = name
+
+    def __enter__(self):
+        """Emit fold start syntax."""
+        print("travis_fold:start:%s" % self.name, file=sys.stderr)
+
+    def __exit__(self, type, value, traceback):
+        """Emit fold end syntax."""
+        print("travis_fold:end:%s" % self.name, file=sys.stderr)
+
+
+class FilteredIO(object):
+    """Wrap a file object, invoking the provided callback for every call to
+    `write` and only proceeding with the operation when that callback returns
+    True."""
+    def __init__(self, original, on_write):
+        self.original = original
+        self.on_write = on_write
+
+    def __getattr__(self, name):
+        return getattr(self.original, name)
+
+    def disable(self):
+        self.write = lambda msg: None
+
+    def write(self, msg):
+        encoded = msg.encode("utf8", "backslashreplace").decode("utf8")
+        if self.on_write(self.original, encoded) is True:
+            self.original.write(encoded)
+
+
+def replace_streams(capacity, warning_msg):
+    # Value must be boxed to support modification from inner function scope
+    count = [0]
+    capacity -= 2 + len(warning_msg)
+    stderr = sys.stderr
+
+    def on_write(handle, msg):
+        length = len(msg)
+        count[0] += length
+
+        if count[0] > capacity:
+            wrapped_stdout.disable()
+            wrapped_stderr.disable()
+            handle.write(msg[0:capacity - count[0]])
+            handle.flush()
+            stderr.write("\n%s\n" % warning_msg)
+            return False
+
+        return True
+
+    # Store local references to the replaced streams to guard against the case
+    # where other code replace the global references.
+    sys.stdout = wrapped_stdout = FilteredIO(sys.stdout, on_write)
+    sys.stderr = wrapped_stderr = FilteredIO(sys.stderr, on_write)
+
+
+def call(*args):
+    """Log terminal command, invoke it as a subprocess.
+
+    Returns a bytestring of the subprocess output if no error.
+    """
+    logger.debug("%s" % " ".join(args))
+    try:
+        return subprocess.check_output(args)
+    except subprocess.CalledProcessError as e:
+        logger.critical("%s exited with return code %i" %
+                        (e.cmd, e.returncode))
+        logger.critical(e.output)
+        raise
+
+def fetch_wpt(user, *args):
+    git = get_git_cmd(wpt_root)
+    git("fetch", "https://github.com/%s/web-platform-tests.git" % user, *args)
+
+
+def get_sha1():
+    """ Get and return sha1 of current git branch HEAD commit."""
+    git = get_git_cmd(wpt_root)
+    return git("rev-parse", "HEAD").strip()
+
+
+def install_wptrunner():
+    """Install wptrunner."""
+    call("pip", "install", wptrunner_root)
+
+
+def deepen_checkout(user):
+    """Convert from a shallow checkout to a full one"""
+    fetch_args = [user, "+refs/heads/*:refs/remotes/origin/*"]
+    if os.path.exists(os.path.join(wpt_root, ".git", "shallow")):
+        fetch_args.insert(1, "--unshallow")
+    fetch_wpt(*fetch_args)
+
+
+def get_parser():
+    """Create and return script-specific argument parser."""
+    description = """Detect instabilities in new tests by executing tests
+    repeatedly and comparing results between executions."""
+    parser = argparse.ArgumentParser(description=description)
+    parser.add_argument("--user",
+                        action="store",
+                        # Travis docs say do not depend on USER env variable.
+                        # This is a workaround to get what should be the same value
+                        default=os.environ.get("TRAVIS_REPO_SLUG", "w3c").split('/')[0],
+                        help="Travis user name")
+    parser.add_argument("--output-bytes",
+                        action="store",
+                        type=int,
+                        help="Maximum number of bytes to write to standard output/error")
+    parser.add_argument("--metadata",
+                        dest="metadata_root",
+                        action="store",
+                        default=wpt_root,
+                        help="Directory that will contain MANIFEST.json")
+    parser.add_argument("--config-file",
+                        action="store",
+                        type=str,
+                        help="Location of ini-formatted configuration file",
+                        default="check_stability.ini")
+    parser.add_argument("--rev",
+                        action="store",
+                        default=None,
+                        help="Commit range to use")
+    return parser
+
+
+def set_default_args(kwargs):
+    kwargs.set_if_none("sauce_platform",
+                       os.environ.get("PLATFORM"))
+    kwargs.set_if_none("sauce_build",
+                       os.environ.get("TRAVIS_BUILD_NUMBER"))
+    python_version = os.environ.get("TRAVIS_PYTHON_VERSION")
+    kwargs.set_if_none("sauce_tags",
+                       [python_version] if python_version else [])
+    kwargs.set_if_none("sauce_tunnel_id",
+                       os.environ.get("TRAVIS_JOB_NUMBER"))
+    kwargs.set_if_none("sauce_user",
+                       os.environ.get("SAUCE_USERNAME"))
+    kwargs.set_if_none("sauce_key",
+                       os.environ.get("SAUCE_ACCESS_KEY"))
+
+
+def pr():
+    pr = os.environ.get("TRAVIS_PULL_REQUEST", "false")
+    return pr if pr != "false" else None
+
+
+def post_results(results, pr_number, iterations, product, url, status):
+    """Post stability results to a given URL."""
+    payload_results = []
+
+    for test_name, test in results.iteritems():
+        subtests = []
+        for subtest_name, subtest in test['subtests'].items():
+            subtests.append({
+                'test': subtest_name,
+                'result': {
+                    'messages': list(subtest['messages']),
+                    'status': subtest['status']
+                },
+            })
+        payload_results.append({
+            'test': test_name,
+            'result': {
+                'status': test['status'],
+                'subtests': subtests
+            }
+        })
+
+    payload = {
+        "pull": {
+            "number": int(pr_number),
+            "sha": os.environ.get("TRAVIS_PULL_REQUEST_SHA"),
+        },
+        "job": {
+            "id": int(os.environ.get("TRAVIS_JOB_ID")),
+            "number": os.environ.get("TRAVIS_JOB_NUMBER"),
+            "allow_failure": os.environ.get("TRAVIS_ALLOW_FAILURE") == 'true',
+            "status": status,
+        },
+        "build": {
+            "id": int(os.environ.get("TRAVIS_BUILD_ID")),
+            "number": os.environ.get("TRAVIS_BUILD_NUMBER"),
+        },
+        "product": product,
+        "iterations": iterations,
+        "message": "All results were stable." if status == "passed" else "Unstable results.",
+        "results": payload_results,
+    }
+
+    requests.post(url, json=payload)
+
+
+def main():
+    """Perform check_stability functionality and return exit code."""
+
+    venv = Virtualenv(os.environ.get("VIRTUAL_ENV", os.path.join(wpt_root, "_venv")))
+    venv.install_requirements(os.path.join(wpt_root, "tools", "wptrunner", "requirements.txt"))
+    venv.install("requests")
+
+    args, wpt_args = get_parser().parse_known_args()
+    return run(venv, wpt_args, **vars(args))
+
+
+def run(venv, wpt_args, **kwargs):
+    global logger
+
+    do_delayed_imports()
+
+    retcode = 0
+    parser = get_parser()
+
+    wpt_args = create_parser().parse_args(wpt_args)
+
+    with open(kwargs["config_file"], 'r') as config_fp:
+        config = SafeConfigParser()
+        config.readfp(config_fp)
+        skip_tests = config.get("file detection", "skip_tests").split()
+        ignore_changes = set(config.get("file detection", "ignore_changes").split())
+        results_url = config.get("file detection", "results_url")
+
+    if kwargs["output_bytes"] is not None:
+        replace_streams(kwargs["output_bytes"],
+                        "Log reached capacity (%s bytes); output disabled." % kwargs["output_bytes"])
+
+
+    wpt_args.metadata_root = kwargs["metadata_root"]
+    try:
+        os.makedirs(wpt_args.metadata_root)
+    except OSError:
+        pass
+
+    logger = logging.getLogger(os.path.splitext(__file__)[0])
+
+    setup_logging()
+
+    browser_name = wpt_args.product.split(":")[0]
+
+    if browser_name == "sauce" and not wpt_args.sauce_key:
+        logger.warning("Cannot run tests on Sauce Labs. No access key.")
+        return retcode
+
+    pr_number = pr()
+
+    with TravisFold("browser_setup"):
+        logger.info(markdown.format_comment_title(wpt_args.product))
+
+        if pr is not None:
+            deepen_checkout(kwargs["user"])
+
+        # Ensure we have a branch called "master"
+        fetch_wpt(kwargs["user"], "master:master")
+
+        head_sha1 = get_sha1()
+        logger.info("Testing web-platform-tests at revision %s" % head_sha1)
+
+        if not kwargs["rev"]:
+            branch_point = testfiles.branch_point()
+            revish = "%s..HEAD" % branch_point
+        else:
+            revish = kwargs["rev"]
+
+        files_changed, files_ignored = testfiles.files_changed(revish, ignore_changes)
+
+        if files_ignored:
+            logger.info("Ignoring %s changed files:\n%s" % (len(files_ignored),
+                                                            "".join(" * %s\n" % item for item in files_ignored)))
+
+        tests_changed, files_affected = testfiles.affected_testfiles(files_changed, skip_tests,
+                                                                     manifest_path=os.path.join(
+                                                                         wpt_args.metadata_root,
+                                                                         "MANIFEST.json"))
+
+        if not (tests_changed or files_affected):
+            logger.info("No tests changed")
+            return 0
+
+        wpt_kwargs = Kwargs(vars(wpt_args))
+        wpt_kwargs["test_list"] = list(tests_changed | files_affected)
+        set_default_args(wpt_kwargs)
+
+        do_delayed_imports()
+
+        wpt_kwargs["stability"] = True
+        wpt_kwargs["prompt"] = False
+        wpt_kwargs["install_browser"] = True
+        wpt_kwargs["install"] = wpt_kwargs["product"].split(":")[0] == "firefox"
+
+        wpt_kwargs = setup_wptrunner(venv, **wpt_kwargs)
+
+        logger.info("Using binary %s" % wpt_kwargs["binary"])
+
+        if tests_changed:
+            logger.debug("Tests changed:\n%s" % "".join(" * %s\n" % item for item in tests_changed))
+
+        if files_affected:
+            logger.debug("Affected tests:\n%s" % "".join(" * %s\n" % item for item in files_affected))
+
+
+    with TravisFold("running_tests"):
+        logger.info("Starting tests")
+
+
+        wpt_logger = wptrunner.logger
+        iterations, results, inconsistent = run(venv, wpt_logger, **wpt_kwargs)
+
+    if results:
+        if inconsistent:
+            write_inconsistent(logger.error, inconsistent, iterations)
+            retcode = 2
+        else:
+            logger.info("All results were stable\n")
+        with TravisFold("full_results"):
+            write_results(logger.info, results, iterations,
+                          pr_number=pr_number,
+                          use_details=True)
+            if pr_number:
+                post_results(results, iterations=iterations, url=results_url,
+                             product=wpt_args.product, pr_number=pr_number,
+                             status="failed" if inconsistent else "passed")
+    else:
+        logger.info("No tests run.")
+
+    return retcode
+
+
+if __name__ == "__main__":
+    try:
+        retcode = main()
+    except:
+        import traceback
+        traceback.print_exc()
+        sys.exit(1)
+    else:
+        sys.exit(retcode)
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_built_diff.sh b/src/third_party/web_platform_tests/tools/ci/ci_built_diff.sh
new file mode 100755
index 0000000..ce9fe60
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_built_diff.sh
@@ -0,0 +1,25 @@
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+main() {
+    # Diff PNGs based on pixel-for-pixel identity
+    echo -e '[diff "img"]\n  textconv = identify -quiet -format "%#"' >> .git/config
+    echo -e '*.png diff=img' >> .git/info/attributes
+
+    # Exclude tests that rely on font rendering
+    excluded=(
+        '2dcontext/drawing-text-to-the-canvas/2d.text.draw.fill.basic.png'
+        '2dcontext/drawing-text-to-the-canvas/2d.text.draw.fill.maxWidth.large.png'
+        '2dcontext/drawing-text-to-the-canvas/2d.text.draw.fill.rtl.png'
+        '2dcontext/drawing-text-to-the-canvas/2d.text.draw.stroke.basic.png'
+    )
+
+    ./update-built-tests.sh
+    git update-index --assume-unchanged ${excluded[*]}
+    git diff --exit-code
+}
+
+main
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_lint.sh b/src/third_party/web_platform_tests/tools/ci/ci_lint.sh
new file mode 100644
index 0000000..46b317e
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_lint.sh
@@ -0,0 +1,9 @@
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+mkdir -p ~/meta
+./wpt manifest -p ~/meta/MANIFEST.json
+./wpt lint
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_resources_unittest.sh b/src/third_party/web_platform_tests/tools/ci/ci_resources_unittest.sh
new file mode 100755
index 0000000..fd28db5
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_resources_unittest.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+main() {
+    cd $WPT_ROOT
+    pip install -U tox
+    pip install --requirement tools/wpt/requirements.txt
+    ./wpt install firefox browser --destination $HOME
+    ./wpt install firefox webdriver --destination $HOME/firefox
+    export PATH=$HOME/firefox:$PATH
+
+    cd $WPT_ROOT/resources/test
+    tox
+}
+
+main
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_stability.sh b/src/third_party/web_platform_tests/tools/ci/ci_stability.sh
new file mode 100644
index 0000000..c83dad3
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_stability.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+source tools/ci/lib.sh
+
+test_stability() {
+    ./wpt check-stability $PRODUCT --output-bytes $((1024 * 1024 * 3)) --metadata ~/meta/
+}
+
+main() {
+    hosts_fixup
+    if [ $(echo $PRODUCT | grep '^chrome:') ]; then
+       install_chrome $(echo $PRODUCT | grep --only-matching '\w\+$')
+    fi
+    test_stability
+}
+
+main
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_tools_unittest.sh b/src/third_party/web_platform_tests/tools/ci/ci_tools_unittest.sh
new file mode 100755
index 0000000..6c74a25
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_tools_unittest.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+if [[ $(./wpt test-jobs --includes tools_unittest; echo $?) -eq 0 ]]; then
+    pip install -U tox codecov
+    cd tools
+    tox
+    cd $WPT_ROOT
+else
+    echo "Skipping tools unittest"
+fi
+
+if [[ $(./wpt test-jobs --includes wptrunner_unittest; echo $?) -eq 0 ]]; then
+    if [ $TOXENV == "py27" ] || [ $TOXENV == "pypy" ]; then
+        cd tools/wptrunner
+        tox
+    fi
+else
+    echo "Skipping wptrunner unittest"
+fi
+
diff --git a/src/third_party/web_platform_tests/tools/ci/ci_wpt.sh b/src/third_party/web_platform_tests/tools/ci/ci_wpt.sh
new file mode 100644
index 0000000..2b63413
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/ci_wpt.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+source tools/ci/lib.sh
+
+main() {
+    git fetch --unshallow https://github.com/w3c/web-platform-tests.git +refs/heads/*:refs/remotes/origin/*
+    hosts_fixup
+    install_chrome unstable
+    pip install -U tox codecov
+    cd tools/wpt
+    tox
+}
+
+main
diff --git a/src/third_party/web_platform_tests/tools/ci/commands.json b/src/third_party/web_platform_tests/tools/ci/commands.json
new file mode 100644
index 0000000..d682d2a
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/commands.json
@@ -0,0 +1,6 @@
+{
+    "test-jobs": {"path": "jobs.py", "script": "run", "parser": "create_parser", "help": "List test jobs that should run for a set of commits",
+                  "virtualenv": false},
+    "check-stability": {"path": "check_stability.py", "script": "run", "parser": "get_parser", "parse_known": true, "help": "Check test stability",
+                        "virtualenv": true, "install": ["requests"], "requirements": ["../wptrunner/requirements.txt"]}
+}
diff --git a/src/third_party/web_platform_tests/tools/ci/install.sh b/src/third_party/web_platform_tests/tools/ci/install.sh
new file mode 100755
index 0000000..d1d4368
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/install.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+if [[ $RUN_JOB -eq 1 ]]; then
+    pip install -U setuptools
+    pip install -U requests
+fi
diff --git a/src/third_party/web_platform_tests/tools/ci/jobs.py b/src/third_party/web_platform_tests/tools/ci/jobs.py
new file mode 100644
index 0000000..2d2ef2a
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/jobs.py
@@ -0,0 +1,119 @@
+import argparse
+import os
+import re
+from ..wpt.testfiles import branch_point, files_changed, affected_testfiles
+
+from tools import localpaths
+from six import iteritems
+
+wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
+
+# Rules are just regex on the path, with a leading ! indicating a regex that must not
+# match for the job
+job_path_map = {
+    "stability": [".*/.*",
+                  "!tools/",
+                  "!docs/",
+                  "!resources/*",
+                  "!conformance-checkers/",
+                  "!.*/OWNERS",
+                  "!.*/tools/",
+                  "!.*/README",
+                  "!css/[^/]*$"],
+    "lint": [".*"],
+    "resources_unittest": ["resources/"],
+    "tools_unittest": ["tools/"],
+    "wptrunner_unittest": ["tools/wptrunner/*"],
+    "build_css": ["css/"],
+    "update_built": ["2dcontext/",
+                     "assumptions/",
+                     "html/",
+                     "offscreen-canvas/"],
+    "wpt_integration": ["tools/"],
+}
+
+
+class Ruleset(object):
+    def __init__(self, rules):
+        self.include = []
+        self.exclude = []
+        for rule in rules:
+            self.add_rule(rule)
+
+    def add_rule(self, rule):
+        if rule.startswith("!"):
+            target = self.exclude
+            rule = rule[1:]
+        else:
+            target = self.include
+
+        target.append(re.compile("^%s" % rule))
+
+    def __call__(self, path):
+        if os.path.sep != "/":
+            path = path.replace(os.path.sep, "/")
+        path = os.path.normcase(path)
+        for item in self.exclude:
+            if item.match(path):
+                return False
+        for item in self.include:
+            if item.match(path):
+                return True
+        return False
+
+    def __repr__(self):
+        subs = tuple(",".join(item.pattern for item in target)
+                     for target in (self.include, self.exclude))
+        return "Rules<include:[%s] exclude:[%s]>" % subs
+
+
+def get_paths(**kwargs):
+    if kwargs["revish"] is None:
+        revish = "%s..HEAD" % branch_point()
+    else:
+        revish = kwargs["revish"]
+
+    changed, _ = files_changed(revish)
+    all_changed = set(os.path.relpath(item, wpt_root)
+                      for item in set(changed))
+    return all_changed
+
+
+def get_jobs(paths, **kwargs):
+    jobs = set()
+
+    rules = {}
+    includes = kwargs.get("includes")
+    if includes is not None:
+        includes = set(includes)
+    for key, value in iteritems(job_path_map):
+        if includes is None or key in includes:
+            rules[key] = Ruleset(value)
+
+    for path in paths:
+        for job in list(rules.keys()):
+            ruleset = rules[job]
+            if ruleset(path):
+                rules.pop(job)
+                jobs.add(job)
+        if not rules:
+            break
+
+    return jobs
+
+
+def create_parser():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("revish", default=None, help="Commits to consider. Defaults to the commits on the current branch", nargs="?")
+    parser.add_argument("--includes", default=None, help="Jobs to check for. Return code is 0 if all jobs are found, otherwise 1", nargs="*")
+    return parser
+
+
+def run(**kwargs):
+    paths = get_paths(**kwargs)
+    jobs = get_jobs(paths, **kwargs)
+    if not kwargs["includes"]:
+        for item in sorted(jobs):
+            print(item)
+    else:
+        return 0 if set(kwargs["includes"]) == jobs else 1
diff --git a/src/third_party/web_platform_tests/tools/ci/lib.sh b/src/third_party/web_platform_tests/tools/ci/lib.sh
new file mode 100644
index 0000000..e80499a
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/lib.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+hosts_fixup() {
+    echo "travis_fold:start:hosts_fixup"
+    echo "Rewriting hosts file"
+    echo "## /etc/hosts ##"
+    cat /etc/hosts
+    sudo sed -i 's/^::1\s*localhost/::1/' /etc/hosts
+    sudo sh -c 'echo "
+127.0.0.1 web-platform.test
+127.0.0.1 www.web-platform.test
+127.0.0.1 www1.web-platform.test
+127.0.0.1 www2.web-platform.test
+127.0.0.1 xn--n8j6ds53lwwkrqhv28a.web-platform.test
+127.0.0.1 xn--lve-6lad.web-platform.test
+0.0.0.0 nonexistent-origin.web-platform.test
+" >> /etc/hosts'
+    echo "== /etc/hosts =="
+    cat /etc/hosts
+    echo "----------------"
+    echo "travis_fold:end:hosts_fixup"
+}
+
+install_chrome() {
+    channel=$1
+    deb_archive=google-chrome-${channel}_current_amd64.deb
+    wget https://dl.google.com/linux/direct/$deb_archive
+
+    # If the environment provides an installation of Google Chrome, the
+    # existing binary may take precedence over the one introduced in this
+    # script. Remove any previously-existing "alternatives" prior to
+    # installation in order to ensure that the new binary is installed as
+    # intended.
+    if sudo update-alternatives --list google-chrome; then
+        sudo update-alternatives --remove-all google-chrome
+    fi
+
+    # Installation will fail in cases where the package has unmet dependencies.
+    # When this occurs, attempt to use the system package manager to fetch the
+    # required packages and retry.
+    if ! sudo dpkg --install $deb_archive; then
+      sudo apt-get install --fix-broken
+      sudo dpkg --install $deb_archive
+    fi
+}
diff --git a/src/third_party/web_platform_tests/tools/ci/run.sh b/src/third_party/web_platform_tests/tools/ci/run.sh
new file mode 100755
index 0000000..d126dbe
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/run.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+set -ex
+
+SCRIPT_DIR=$(dirname $(readlink -f "$0"))
+WPT_ROOT=$(readlink -f $SCRIPT_DIR/../..)
+cd $WPT_ROOT
+
+if [[ $RUN_JOB -eq 1 ]]; then
+    . $SCRIPT
+fi
diff --git a/src/third_party/web_platform_tests/tools/ci/tests/test_jobs.py b/src/third_party/web_platform_tests/tools/ci/tests/test_jobs.py
new file mode 100644
index 0000000..3192a47
--- /dev/null
+++ b/src/third_party/web_platform_tests/tools/ci/tests/test_jobs.py
@@ -0,0 +1,76 @@
+from tools.ci import jobs
+
+def test_testharness():
+    assert jobs.get_jobs(["resources/testharness.js"]) == set(["lint", "resources_unittest"])
+    assert jobs.get_jobs(["resources/testharness.js"],
+                         includes=["resources_unittest"]) == set(["resources_unittest"])
+    assert jobs.get_jobs(["foo/resources/testharness.js"],
+                         includes=["resources_unittest"]) == set()
+
+def test_stability():
+    assert jobs.get_jobs(["dom/historical.html"],
+                         includes=["stability"]) == set(["stability"])
+    assert jobs.get_jobs(["tools/pytest.ini"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["serve"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["resources/testharness.js"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["docs/.gitignore"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["dom/tools/example.py"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["conformance-checkers/test.html"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["dom/README.md"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["css/build-css-testsuite.sh"],
+                         includes=["stability"]) == set()
+    assert jobs.get_jobs(["css/CSS21/test-001.html"],
+                         includes=["stability"]) == set(["stability"])
+    assert jobs.get_jobs(["css/build-css-testsuite.sh",
+                          "css/CSS21/test-001.html"],
+                         includes=["stability"]) == set(["stability"])
+
+def test_lint():
+    assert jobs.get_jobs(["README.md"]) == set(["lint"])
+
+def test_tools_unittest():
+    assert jobs.get_jobs(["tools/ci/test/test_jobs.py"],
+                         includes=["tools_unittest"]) == set(["tools_unittest"])
+    assert jobs.get_jobs(["dom/tools/example.py"],
+                         includes=["tools_unittest"]) == set()
+    assert jobs.get_jobs(["dom/historical.html"],
+                         includes=["tools_unittest"]) == set()
+
+def test_wptrunner_unittest():
+    assert jobs.get_jobs(["tools/wptrunner/wptrunner/wptrunner.py"],
+                         includes=["wptrunner_unittest"]) == set(["wptrunner_unittest"])
+    assert jobs.get_jobs(["tools/example.py"],
+                         includes=["wptrunner_unittest"]) == set()
+
+def test_build_css():
+    assert jobs.get_jobs(["css/css-build-testsuites.sh"],
+                         includes=["build_css"]) == set(["build_css"])
+    assert jobs.get_jobs(["css/CSS21/test.html"],
+                         includes=["build_css"]) == set(["build_css"])
+    assert jobs.get_jobs(["html/css/CSS21/test.html"],
+                         includes=["build_css"]) == set()
+
+
+def test_update_built():
+    assert jobs.get_jobs(["2dcontext/foo.html"],
+                         includes=["update_built"]) == set(["update_built"])
+    assert jobs.get_jobs(["assumptions/foo.html"],
+                         includes=["update_built"]) == set(["update_built"])
+    assert jobs.get_jobs(["html/foo.html"],
+                         includes=["update_built"]) == set(["update_built"])
+    assert jobs.get_jobs(["offscreen-canvas/foo.html"],
+                         includes=["update_built"]) == set(["update_built"])
+
+
+def test_wpt_integration():
+    assert jobs.get_jobs(["tools/wpt/wpt.py"],
+                         includes=["wpt_integration"]) == set(["wpt_integration"])
+    assert jobs.get_jobs(["tools/wptrunner/wptrunner/wptrunner.py"],
+                         includes=["wpt_integration"]) == set(["wpt_integration"])