| #!/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:])) |