blob: ca461fadf7a16357ca012a5c507df7058fee2229 [file] [log] [blame]
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Chromite main test runner.
Run the specified tests. If none are specified, we'll scan the
tree looking for tests to run and then only run the semi-fast ones.
https://docs.pytest.org/en/latest/how-to/usage.html#specifying-tests-selecting-tests
Examples:
# Run all tests in a module.
$ ./run_tests lib/osutils_unittest.py
# Run a class of tests in a module.
$ ./run_tests lib/osutils_unittest.py::TestOsutils
# Run a single test.
$ ./run_tests lib/osutils_unittest.py::TestOsutils::testIsSubPath
# List all tests that'd be run.
$ ./run_tests -- --collect-only
"""
import logging
import os
import sys
import pytest # pylint: disable=import-error
from chromite.api import compile_build_api_proto
from chromite.format import formatters
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import gs
from chromite.lib import namespaces
from chromite.lint import linters
from chromite.scripts import clang_format
def main(argv):
parser = get_parser()
opts = parser.parse_args()
opts.Freeze()
pytest_args = opts.pytest_args
if opts.chroot:
ensure_chroot_exists()
re_execute_inside_chroot(argv)
else:
pytest_args += ["--no-chroot"]
if opts.network:
pytest_args += ["-m", "not network_test or network_test"]
if opts.precache:
precache()
if opts.quick:
logging.info("Skipping test namespacing due to --quickstart.")
else:
# Namespacing is enabled by default because tests may break each other
# or interfere with parts of the running system if not isolated in a
# namespace. Disabling namespaces is not recommended for general use.
namespaces.ReExecuteWithNamespace(
[sys.argv[0], "--no-precache"] + argv, network=opts.network
)
jobs = opts.jobs
if jobs is None:
# Default to running in a single process under --quickstart. User args
# can still override this.
jobs = 0 if opts.quick else os.cpu_count()
pytest_args = ["-n", str(jobs)] + pytest_args
# Check the environment. https://crbug.com/1015450
st = os.stat("/")
if st.st_mode & 0o007 != 0o005:
cros_build_lib.Die(
f"The root directory has broken permissions: {st.st_mode:o}\n"
"Fix with: sudo chmod o+rx-w /"
)
if st.st_uid or st.st_gid:
cros_build_lib.Die(
f"The root directory has broken ownership: {st.st_uid}:{st.st_gid}"
" (should be 0:0)\nFix with: sudo chown 0:0 /"
)
logging.debug("Running: pytest %s", cros_build_lib.CmdToStr(pytest_args))
sys.exit(pytest.main(pytest_args))
def precache():
"""Do some network-dependent stuff before we disallow network access."""
# pylint: disable=protected-access
logging.notice("Caching tools from network (cipd/vpython/etc...)")
# This is a cheesy hack to make sure gsutil is populated in the cache before
# we run tests. This is a partial workaround for crbug.com/468838.
gs.GSContext.InitializeCache()
# Ensure protoc is installed for api/compile_build_api_proto_unittest.
compile_build_api_proto.InstallProtoc(
compile_build_api_proto.ProtocVersion.CHROMITE
)
# Ensure various tools are available.
cros_build_lib.dbg_run(
[constants.CHROMITE_DIR / "scripts" / "black", "--version"],
capture_output=True,
)
cros_build_lib.dbg_run(
[constants.CHROMITE_DIR / "scripts" / "isort", "--version"],
capture_output=True,
)
formatters.gn._find_gn()
formatters.star._find_buildifier()
formatters.textproto._find_txtpbfmt()
linters.shell._find_shellcheck()
with clang_format.ClangFormat():
pass
def re_execute_inside_chroot(argv):
"""Re-execute the test wrapper inside the chroot."""
if cros_build_lib.IsInsideChroot():
return
target = constants.CHROMITE_DIR / "scripts" / "run_tests"
relpath = os.path.relpath(target, ".")
# If we're in the scripts dir, make sure we always have a relative path,
# otherwise cros_sdk will search $PATH and fail.
if os.path.sep not in relpath:
relpath = os.path.join(".", relpath)
cmd = [
"cros_sdk",
"--working-dir",
".",
"--",
relpath,
]
os.execvp(cmd[0], cmd + argv)
def ensure_chroot_exists():
"""Ensure that a chroot exists for us to run tests in."""
chroot = os.path.join(constants.SOURCE_ROOT, constants.DEFAULT_CHROOT_DIR)
if not os.path.exists(chroot) and not cros_build_lib.IsInsideChroot():
cros_build_lib.run(["cros_sdk", "--create"])
def get_parser():
"""Build the parser for command line arguments."""
parser = commandline.ArgumentParser(
description=__doc__,
epilog="To see the help output for pytest:\n$ %(prog)s -- --help",
)
parser.add_argument(
"-j",
"--jobs",
type=int,
default=None,
help="Number of tests to run in parallel.",
)
parser.add_argument(
"--quickstart",
dest="quick",
action="store_true",
help=(
"Skip normal test sandboxing and namespacing for faster start up "
"time."
),
)
parser.add_argument(
"--network",
action="store_true",
help="Include network tests.",
)
parser.add_argument(
"--no-precache",
dest="precache",
action="store_false",
help="Skip precaching packages from the network.",
)
parser.add_argument(
"--no-chroot",
dest="chroot",
action="store_false",
help=(
"Don't initialize or enter a chroot for the test invocation. May "
"cause tests to unexpectedly fail!"
),
)
parser.add_argument(
"pytest_args",
metavar="pytest arguments",
nargs="*",
help="Arguments to pass down to pytest (use -- to help separate)",
)
return parser