blob: 10e25bddb1e58e3231c52dbe00501dda54b0a58a [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Run unittests in a new browser."""
import argparse
import datetime
import logging
import os
import subprocess
import sys
import libdot
# Path to our html test page.
TEST_PAGE = (libdot.DIR / "html" / "lib_test.html").relative_to(
libdot.LIBAPPS_DIR
)
def get_parser(port: int = 8080):
"""Get a parser for our test runner."""
parser = libdot.ArgumentParser(
description="HTML test runner",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--browser",
default=os.getenv("CHROME_BIN"),
help="Browser program to run tests against.",
)
parser.add_argument(
"--profile",
default=os.getenv("CHROME_TEST_PROFILE"),
help="Browser profile dir to run against.",
)
parser.add_argument(
"--skip-mkdeps",
dest="run_mkdeps",
action="store_false",
default=True,
help="Skip (re)building of dependencies.",
)
parser.add_argument(
"--visible",
action="store_true",
help="Show the browser window to interact with.",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=port,
help="Port to run local server on. (default: %(default)s)",
)
parser.add_argument(
"--reporter",
default="spec",
help="Set the mocha test results report format.",
)
# Note: This CLI option matches Chrome's own naming.
parser.add_argument(
"--no-sandbox",
dest="sandbox",
action="store_false",
default=True,
help="Disable Chrome sandboxing.",
)
return parser
def test_runner_main(argv, path, serve=False, mkdeps=None):
"""Open the test page at |path|.
Args:
argv: The program's command line arguments.
path: Path to the test page.
serve: Whether to launch a webserver or load the page from disk.
mkdeps: Callback to build dependencies after we've initialized.
"""
# Try to provide a default port that doesn't collide with other projects.
# We hash the path to the test page and use that to stay [8000,9000].
parser = get_parser(port=8000 + sum(ord(x) for x in str(path)) % 1000)
opts = parser.parse_args(argv)
# Try to use default X session. We only do this when the browser is visible
# to workaround a bug in Chrome R96+ https://crbug.com/1280727.
if opts.visible:
os.environ.setdefault("DISPLAY", ":0")
else:
os.environ.pop("DISPLAY", None)
# Ensure chai/mocha node modules exist.
libdot.node_and_npm_setup()
# Set up any deps.
if mkdeps:
if opts.run_mkdeps:
mkdeps(opts)
else:
logging.info("Skipping building dependencies due to --skip-mkdeps")
# Set up a unique profile to avoid colliding with user settings.
profile_dir = opts.profile
if not profile_dir:
profile_dir = os.path.expanduser("~/.config/google-chrome-run_local")
os.makedirs(profile_dir, exist_ok=True)
# Find a Chrome version to run against.
browser = opts.browser
if not browser:
browser = libdot.headless_chrome.chrome_setup()
start_time = datetime.datetime.utcnow()
# Kick off server if needed.
if serve:
# We manage the life-cycle of this ourselves below.
# pylint: disable=consider-using-with
server = subprocess.Popen(
[
libdot.node.NODE,
os.path.join(libdot.node.NODE_BIN_DIR, "http-server"),
"--cors",
"-a",
"localhost",
"-c-1",
f"-p{opts.port}",
],
cwd=libdot.LIBAPPS_DIR,
)
path = f"http://localhost:{opts.port}/{path}"
# Some environments are unable to utilize the sandbox: we're not running as
# root, and userns is unavailable. For example, while using docker.
if opts.sandbox:
sb_arg = mocha_sb_arg = []
else:
sb_arg = ["--no-sandbox"]
# The wrapper requires omitting the leading dashes for no real reason.
mocha_sb_arg = ["--args=no-sandbox"]
try:
# Kick off test runner in the background so we exit.
logging.info('Running tests against browser "%s".', browser)
logging.info("Tests page: %s", path)
if opts.visible:
cmd = [browser, f"--user-data-dir={profile_dir}", path] + sb_arg
logging.info(
"Running: %s\n (cwd = %s)",
libdot.cmdstr(cmd),
os.getcwd(),
)
# We want to orphan this process.
# pylint: disable=consider-using-with
subprocess.Popen(cmd)
else:
# The standalone mocha runner doesn't have a timeout. The headless
# one defaults to 1 minute which is too slow for some of our suites.
# Increase it to 5 minutes as that should be good enough for all.
cmd = [
"mocha-headless-chrome",
"-e",
browser,
"-f",
path,
"--reporter",
opts.reporter,
"--timeout=300000",
] + mocha_sb_arg
# Don't bother specifying this if the user hasn't set an option
# since the framework takes care of providing a scratch dir.
if opts.profile:
cmd += [f"--args=user-data-dir={profile_dir}"]
libdot.node.run(cmd)
finally:
# Wait for the server if it exists.
if serve:
if opts.visible:
try:
server.wait()
except KeyboardInterrupt:
pass
else:
server.terminate()
end_time = datetime.datetime.utcnow()
delta = end_time - start_time
logging.info("Tests took %s", delta)
def _mkdeps(_opts) -> None:
"""Build the required deps for the test suite."""
subprocess.check_call([libdot.BIN_DIR / "mkdist"])
def main(argv):
"""The main func!"""
return test_runner_main(argv, TEST_PAGE, serve=True, mkdeps=_mkdeps)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))