summaryrefslogtreecommitdiff
path: root/util/twister_launcher.py
diff options
context:
space:
mode:
Diffstat (limited to 'util/twister_launcher.py')
-rwxr-xr-xutil/twister_launcher.py317
1 files changed, 317 insertions, 0 deletions
diff --git a/util/twister_launcher.py b/util/twister_launcher.py
new file mode 100755
index 0000000000..d98768fed5
--- /dev/null
+++ b/util/twister_launcher.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env vpython3
+
+# Copyright 2022 The ChromiumOS Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+This script is a wrapper for invoking Twister, the Zephyr test runner, using
+default parameters for the ChromiumOS EC project. For an overview of CLI
+parameters that may be used, please consult the Twister documentation.
+"""
+
+# [VPYTHON:BEGIN]
+# python_version: "3.8"
+# wheel: <
+# name: "infra/python/wheels/anytree-py2_py3"
+# version: "version:2.8.0"
+# >
+# wheel: <
+# name: "infra/python/wheels/colorama-py3"
+# version: "version:0.4.1"
+# >
+# wheel: <
+# name: "infra/python/wheels/docopt-py2_py3"
+# version: "version:0.6.2"
+# >
+# wheel: <
+# name: "infra/python/wheels/ply-py2_py3"
+# version: "version:3.11"
+# >
+# wheel: <
+# name: "infra/python/wheels/psutil/${vpython_platform}"
+# version: "version:5.8.0.chromium.3"
+# >
+# wheel: <
+# name: "infra/python/wheels/pykwalify-py2_py3"
+# version: "version:1.8.0"
+# >
+# wheel: <
+# name: "infra/python/wheels/pyserial-py2_py3"
+# version: "version:3.4"
+# >
+# wheel: <
+# name: "infra/python/wheels/python-dateutil-py2_py3"
+# version: "version:2.8.1"
+# >
+# wheel: <
+# name: "infra/python/wheels/pyyaml-py3"
+# version: "version:5.3.1"
+# >
+# wheel: <
+# name: "infra/python/wheels/ruamel_yaml_clib/${vpython_platform}"
+# version: "version:0.2.6"
+# >
+# wheel: <
+# name: "infra/python/wheels/ruamel_yaml-py3"
+# version: "version:0.17.16"
+# >
+# wheel: <
+# name: "infra/python/wheels/six-py2_py3"
+# version: "version:1.16.0"
+# >
+# [VPYTHON:END]
+
+import argparse
+import os
+import re
+import shlex
+import subprocess
+import sys
+import time
+from pathlib import Path
+from shutil import which
+
+
+def find_checkout() -> Path:
+ """Find the location of the source checkout or return None."""
+ cros_checkout = os.environ.get("CROS_WORKON_SRCROOT")
+ if cros_checkout is not None:
+ return Path(cros_checkout)
+
+ # Attempt to locate checkout location relatively if being run outside of chroot.
+ try:
+ cros_checkout = Path(__file__).resolve().parents[4]
+ assert (cros_checkout / "src").exists()
+ return cros_checkout
+ except (IndexError, AssertionError):
+ # Not in the chroot or matching directory structure
+ return None
+
+
+def find_paths():
+ """Find EC base, Zephyr base, and Zephyr modules paths and return as a 3-tuple."""
+
+ # Determine where the source tree is checked out. Will be None if operating outside
+ # of the chroot (e.g. Gitlab builds). In this case, additional paths need to be
+ # passed in through environment variables.
+ cros_checkout = find_checkout()
+
+ if cros_checkout:
+ ec_base = cros_checkout / "src" / "platform" / "ec"
+ zephyr_base = cros_checkout / "src" / "third_party" / "zephyr" / "main"
+ zephyr_modules_dir = cros_checkout / "src" / "third_party" / "zephyr"
+ else:
+ try:
+ ec_base = Path(os.environ["EC_DIR"]).resolve()
+ except KeyError as err:
+ raise RuntimeError(
+ "EC_DIR unspecified. Please pass as env var or use chroot."
+ ) from err
+
+ try:
+ zephyr_base = Path(os.environ["ZEPHYR_BASE"]).resolve()
+ except KeyError as err:
+ raise RuntimeError(
+ "ZEPHYR_BASE unspecified. Please pass as env var or use chroot."
+ ) from err
+
+ try:
+ zephyr_modules_dir = Path(os.environ["MODULES_DIR"]).resolve()
+ except KeyError as err:
+ raise RuntimeError(
+ "MODULES_DIR unspecified. Please pass as env var or use chroot."
+ ) from err
+
+ return (ec_base, zephyr_base, zephyr_modules_dir)
+
+
+def find_modules(mod_dir: Path) -> list:
+ """Find Zephyr modules in the given directory `dir`."""
+
+ modules = []
+ for child in mod_dir.iterdir():
+ if child.is_dir() and (child / "zephyr" / "module.yml").exists():
+ modules.append(child.resolve())
+ return modules
+
+
+def is_tool(name):
+ """Check if 'name' is on PATH and marked executable."""
+ return which(name) is not None
+
+
+def is_rdb_login():
+ """Checks if user is logged into rdb"""
+ cmd = ["rdb", "auth-info"]
+ ret = subprocess.run(cmd, capture_output=True, text=True, check=False)
+
+ if ret.returncode == 0:
+ print("\nrdb auth-info: " + ret.stdout.split("\n")[0])
+ else:
+ print("\nrdb auth-info: " + ret.stderr)
+
+ return ret.returncode == 0
+
+
+def upload_results(ec_base):
+ """Uploads Zephyr Test results to ResultDB"""
+ flag = False
+
+ if is_rdb_login():
+ json_path = ec_base / "twister-out" / "twister.json"
+ cmd = [
+ "rdb",
+ "stream",
+ "-new",
+ "-realm",
+ "chromium:public",
+ "--",
+ str(ec_base / "util/zephyr_to_resultdb.py"),
+ "--result=" + str(json_path),
+ "--upload=True",
+ ]
+
+ start_time = time.time()
+ ret = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ end_time = time.time()
+
+ # Extract URL to test report from captured output
+ rdb_url = re.search(
+ r"(?P<url>https?://[^\s]+)", ret.stderr.split("\n")[0]
+ ).group("url")
+ print(f"\nTEST RESULTS ({end_time - start_time:.3f}s): {rdb_url}\n")
+ flag = ret.returncode == 0
+ else:
+ print("Unable to upload test results, please run 'rdb auth-login'\n")
+
+ return flag
+
+
+def main():
+ """Run Twister using defaults for the EC project."""
+
+ # Get paths for the build.
+ ec_base, zephyr_base, zephyr_modules_dir = find_paths()
+
+ zephyr_modules = find_modules(zephyr_modules_dir)
+
+ # Add the EC dir as a module if not already included (resolve all paths to
+ # account for symlinked or relative paths)
+ if ec_base.resolve() not in zephyr_modules:
+ zephyr_modules.append(ec_base)
+
+ # Prepare environment variables for export to Twister. Inherit the parent
+ # process's environment, but set some default values if not already set.
+ twister_env = dict(os.environ)
+ is_in_chroot = Path("/etc/cros_chroot_version").is_file()
+ extra_env_vars = {
+ "TOOLCHAIN_ROOT": os.environ.get(
+ "TOOLCHAIN_ROOT",
+ str(ec_base / "zephyr") if is_in_chroot else zephyr_base,
+ ),
+ "ZEPHYR_TOOLCHAIN_VARIANT": os.environ.get(
+ "ZEPHYR_TOOLCHAIN_VARIANT", "llvm" if is_in_chroot else "host"
+ ),
+ }
+ twister_env.update(extra_env_vars)
+
+ # Twister CLI args
+ # TODO(b/239165779): Reduce or remove the usage of label properties
+ # Zephyr upstream has deprecated the label property. We need to allow
+ # warnings during twister runs until all the label properties are removed
+ # from all board and test overlays.
+ twister_cli = [
+ sys.executable,
+ str(zephyr_base / "scripts" / "twister"), # Executable path
+ "--ninja",
+ "--disable-warnings-as-errors",
+ f"-x=DTS_ROOT={str( ec_base / 'zephyr')}",
+ f"-x=SYSCALL_INCLUDE_DIRS={str(ec_base / 'zephyr' / 'include' / 'drivers')}",
+ f"-x=ZEPHYR_BASE={zephyr_base}",
+ f"-x=ZEPHYR_MODULES={';'.join([str(p) for p in zephyr_modules])}",
+ ]
+
+ # `-T` flags (used for specifying test directories to build and run)
+ # require special handling. When run without `-T` flags, Twister will
+ # search for tests in `zephyr_base`. This is undesirable and we want
+ # Twister to look in the EC tree by default, instead. Use argparse to
+ # intercept `-T` flags and pass in a new default if none are found. If
+ # user does pass their own `-T` flags, pass them through instead. Do the
+ # same with verbosity. Other arguments get passed straight through,
+ # including -h/--help so that Twister's own help text gets displayed.
+ parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
+ parser.add_argument("-T", "--testsuite-root", action="append")
+ parser.add_argument("-p", "--platform", action="append")
+ parser.add_argument("-v", "--verbose", action="count", default=0)
+ parser.add_argument(
+ "--gcov-tool", default=str(ec_base / "util" / "llvm-gcov.sh")
+ )
+ parser.add_argument(
+ "--no-upload-cros-rdb", dest="upload_cros_rdb", action="store_false"
+ )
+
+ intercepted_args, other_args = parser.parse_known_args()
+
+ for _ in range(intercepted_args.verbose):
+ # Pass verbosity setting through to twister
+ twister_cli.append("-v")
+
+ if intercepted_args.testsuite_root:
+ # Pass user-provided -T args when present.
+ for arg in intercepted_args.testsuite_root:
+ twister_cli.extend(["-T", arg])
+ else:
+ # Use EC base dir when no -T args specified. This will cause all
+ # Twister-compatible EC tests to run.
+ twister_cli.extend(["-T", str(ec_base)])
+ twister_cli.extend(["-T", str(zephyr_base / "tests/subsys/shell")])
+
+ # Pass through the chosen coverage tool, or fall back on the default choice
+ # (see add_argument above).
+ twister_cli.extend(
+ [
+ "--gcov-tool",
+ intercepted_args.gcov_tool,
+ ]
+ )
+ if intercepted_args.platform:
+ # Pass user-provided -p args when present.
+ for arg in intercepted_args.platform:
+ twister_cli.extend(["-p", arg])
+ else:
+ # posix_native and unit_testing when nothing was requested by user.
+ twister_cli.extend(["-p", "native_posix"])
+ twister_cli.extend(["-p", "unit_testing"])
+
+ # Append additional user-supplied args
+ twister_cli.extend(other_args)
+
+ # Print exact CLI args and environment variables depending on verbosity.
+ if intercepted_args.verbose > 0:
+ print("Calling:", " ".join(shlex.quote(str(x)) for x in twister_cli))
+ print(
+ "With environment overrides:",
+ " ".join(
+ f"{name}={shlex.quote(val)}"
+ for name, val in extra_env_vars.items()
+ ),
+ )
+ sys.stdout.flush()
+
+ # Invoke Twister and wait for it to exit.
+ result = subprocess.run(twister_cli, env=twister_env, check=False)
+
+ if result.returncode == 0:
+ print("TEST EXECUTION SUCCESSFUL")
+ else:
+ print("TEST EXECUTION FAILED")
+
+ if is_tool("rdb") and intercepted_args.upload_cros_rdb:
+ upload_results(ec_base)
+
+ sys.exit(result.returncode)
+
+
+if __name__ == "__main__":
+ main()