blob: 474922ba33829f09ceba0cb55fa994ebebe5a4bd [file] [log] [blame]
#!/usr/bin/env python3
# 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.
import os
import typing
from typing import Generator, List, Literal, Optional, Tuple
from impl.common import (
CROSVM_ROOT,
TOOLS_ROOT,
Triple,
argh,
chdir,
cmd,
is_kiwi_repo,
run_main,
parallel,
)
from impl.presubmit import Check, CheckContext, run_checks, Group
python = cmd("python3")
mypy = cmd("mypy").with_color_env("MYPY_FORCE_COLOR")
black = cmd("black").with_color_arg(always="--color", never="--no-color")
mdformat = cmd("mdformat")
lucicfg = cmd("third_party/depot_tools/lucicfg")
# All supported platforms as a type and a list.
Platform = Literal["x86_64", "aarch64", "mingw64", "armhf", "riscv64"]
PLATFORMS: Tuple[Platform, ...] = typing.get_args(Platform)
def platform_is_supported(platform: Platform):
"Returns true if the platform is available as a target in rustup."
triple = Triple.from_shorthand(platform)
installed_toolchains = cmd("rustup target list --installed").lines()
return str(triple) in installed_toolchains
####################################################################################################
# Check methods
#
# Each check returns a Command (or list of Commands) to be run to execute the check. They are
# registered and configured in the CHECKS list below.
#
# Some check functions are factory functions that return a check command for all supported
# platforms.
def check_python_tests(_: CheckContext):
"Runs unit tests for python dev tooling."
PYTHON_TESTS = [
"tests.cl_tests",
"impl.common",
]
return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS]
def check_python_types(context: CheckContext):
"Run mypy type checks on python dev tooling."
return [mypy("--pretty", file) for file in context.all_files]
def check_python_format(context: CheckContext):
"Runs the black formatter on python dev tooling."
return black.with_args(
"--check" if not context.fix else None,
*context.modified_files,
)
def check_markdown_format(context: CheckContext):
"Runs mdformat on all markdown files."
if "blaze" in mdformat("--version").stdout():
raise Exception(
"You are using google's mdformat. "
+ "Please update your PATH to ensure the pip installed mdformat is available."
)
return mdformat.with_args(
"--wrap 100",
"--check" if not context.fix else "",
*context.modified_files,
)
def check_rust_format(context: CheckContext):
"Runs rustfmt on all modified files."
if context.nightly_fmt:
rustfmt = cmd(
cmd("rustup +nightly which rustfmt"),
"--config imports_granularity=item,group_imports=StdExternalCrate",
)
else:
rustfmt = cmd(cmd("rustup which rustfmt"))
# Windows doesn't accept very long arguments: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#:~:text=The%20maximum%20length%20of%20this%20string%20is%2032%2C767%20characters%2C%20including%20the%20Unicode%20terminating%20null%20character.%20If%20lpApplicationName%20is%20NULL%2C%20the%20module%20name%20portion%20of%20lpCommandLine%20is%20limited%20to%20MAX_PATH%20characters.
return parallel(
*rustfmt.with_color_flag()
.with_args("--check" if not context.fix else "")
.foreach(context.modified_files, batch_size=10)
)
def check_cargo_doc(_: CheckContext):
"Runs cargo-doc and verifies that no warnings are emitted."
return cmd("./tools/cargo-doc").with_env("RUSTDOCFLAGS", "-D warnings").with_color_flag()
def check_doc_tests(_: CheckContext):
"Runs cargo doc tests. These cannot be run via nextest and run_tests."
return cmd(
"cargo test",
"--doc",
"--workspace",
"--features=all-x86_64",
).with_color_flag()
def check_mdbook(_: CheckContext):
"Runs cargo-doc and verifies that no warnings are emitted."
return cmd("mdbook build docs/book/")
def check_crosvm_tests(platform: Platform):
def check(_: CheckContext):
if not platform_is_supported(platform):
return None
dut = None
if os.access("/dev/kvm", os.W_OK):
if platform == "x86_64":
dut = "--dut=vm"
elif platform == "aarch64":
dut = "--dut=vm"
return cmd("./tools/run_tests --verbose --platform", platform, dut).with_color_flag()
check.__doc__ = f"Runs all crosvm tests for {platform}."
return check
def check_crosvm_unit_tests(platform: Platform):
def check(_: CheckContext):
if not platform_is_supported(platform):
return None
command = cmd("./tools/run_tests --verbose --platform", platform).with_color_flag()
if platform == "riscv64":
command = command.with_args("--no-default-features")
return command
check.__doc__ = f"Runs crosvm unit tests for {platform}."
return check
def check_crosvm_build(
platform: Platform, features: Optional[str] = None, no_default_features: bool = False
):
def check(_: CheckContext):
return cmd(
"./tools/run_tests --no-run --verbose --platform",
platform,
f"--features={features}" if features is not None else None,
"--no-default-features" if no_default_features else None,
).with_color_flag()
check.__doc__ = f"Builds crosvm for {platform} with features {features}."
return check
def check_clippy(platform: Platform):
def check(context: CheckContext):
if not platform_is_supported(platform):
return None
return cmd(
"./tools/clippy --platform",
platform,
"--fix" if context.fix else None,
).with_color_flag()
check.__doc__ = f"Runs clippy for {platform}."
return check
def check_infra_configs(context: CheckContext):
"Validate luci configs by sending them to luci-config."
# TODO: Validate config files. Requires authentication with luci inside docker.
return [lucicfg("fmt --dry-run", file) for file in context.modified_files]
def check_infra_tests(_: CheckContext):
"Run recipe.py tests, all of them, regardless of which files were modified."
recipes = cmd("infra/recipes.py").with_path_env("third_party/depot_tools")
return recipes("test run")
def custom_check(name: str, can_fix: bool = False):
"Custom checks are written in python in tools/custom_checks. This is a wrapper to call them."
def check(context: CheckContext):
return cmd(
TOOLS_ROOT / "custom_checks",
name,
*context.modified_files,
"--fix" if can_fix and context.fix else None,
)
check.__name__ = name.replace("-", "_")
check.__doc__ = f"Runs tools/custom_check {name}"
return check
####################################################################################################
# Checks configuration
#
# Configures which checks are available and on which files they are run.
# Check names default to the function name minus the check_ prefix
CHECKS: List[Check] = [
Check(
check_rust_format,
files=["**.rs"],
exclude=["system_api/src/bindings/*"],
can_fix=True,
),
Check(
check_mdbook,
files=["docs/**/*"],
),
Check(
check_cargo_doc,
files=["**.rs", "**Cargo.toml"],
priority=True,
),
Check(
check_doc_tests,
files=["**.rs", "**Cargo.toml"],
priority=True,
),
Check(
check_python_tests,
files=["tools/**.py"],
python_tools=True,
priority=True,
),
Check(
check_python_types,
files=["tools/**.py"],
exclude=[
"tools/windows/*",
"tools/contrib/memstats_chart/*",
"tools/contrib/cros_tracing_analyser/*",
],
python_tools=True,
),
Check(
check_python_format,
files=["**.py"],
python_tools=True,
exclude=["infra/recipes.py"],
can_fix=True,
),
Check(
check_markdown_format,
files=["**.md"],
exclude=[
"infra/README.recipes.md",
"docs/book/src/appendix/memory_layout.md",
],
can_fix=True,
),
*(
Check(
check_crosvm_build(platform, features="default"),
custom_name=f"crosvm_build_default_{platform}",
files=["**.rs"],
priority=True,
)
for platform in PLATFORMS
),
*(
Check(
check_crosvm_build(platform, features="", no_default_features=True),
custom_name=f"crosvm_build_no_default_{platform}",
files=["**.rs"],
priority=True,
)
# TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
for platform in PLATFORMS
if platform != "mingw64"
),
*(
Check(
check_crosvm_tests(platform),
custom_name=f"crosvm_tests_{platform}",
files=["**.rs"],
priority=True,
)
for platform in PLATFORMS
),
*(
Check(
check_crosvm_unit_tests(platform),
custom_name=f"crosvm_unit_tests_{platform}",
files=["**.rs"],
priority=True,
)
for platform in PLATFORMS
),
*(
Check(
check_clippy(platform),
custom_name=f"clippy_{platform}",
files=["**.rs"],
can_fix=True,
priority=True,
)
for platform in PLATFORMS
),
Check(
custom_check("check-copyright-header"),
files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"],
exclude=[
"infra/recipes.py",
"hypervisor/src/whpx/whpx_sys/*.h",
"third_party/vmm_vhost/*",
"net_sys/src/lib.rs",
"system_api/src/bindings/*",
],
python_tools=True,
can_fix=True,
),
Check(
custom_check("check-rust-features"),
files=["**Cargo.toml"],
),
Check(
custom_check("check-rust-lockfiles"),
files=["**Cargo.toml"],
),
Check(
custom_check("check-line-endings"),
),
Check(
custom_check("check-file-ends-with-newline"),
exclude=[
"**.h264",
"**.vp8",
"**.vp9",
"**.ivf",
"**.bin",
"**.png",
"**.min.js",
"**.drawio",
"**.json",
],
),
]
# We disable LUCI infra related tests because kokoro doesn't have internet connectivity that
# the tests rely on.
if not is_kiwi_repo():
CHECKS.extend(
[
Check(
check_infra_configs,
files=["infra/config/**.star"],
can_fix=True,
),
Check(
check_infra_tests,
files=["infra/**.py"],
can_fix=True,
),
]
)
####################################################################################################
# Group configuration
#
# Configures pre-defined groups of checks. Some are configured for CI builders and others
# are configured for convenience during local development.
GROUPS: List[Group] = [
# The default group is run if no check or group is explicitly set
Group(
name="default",
doc="Checks run by default",
checks=[
"default_health_checks",
# Run only one task per platform to prevent blocking on the build cache.
"crosvm_tests_x86_64",
"crosvm_unit_tests_aarch64",
"crosvm_unit_tests_mingw64",
"clippy_armhf",
],
),
Group(
name="quick",
doc="Runs a quick subset of presubmit checks.",
checks=[
"default_health_checks",
"crosvm_unit_tests_x86_64",
"clippy_aarch64",
],
),
Group(
name="all",
doc="Run checks of all builders.",
checks=[
"health_checks",
*(f"linux_{platform}" for platform in PLATFORMS),
],
),
# Convenience groups for local usage:
Group(
name="clippy",
doc="Runs clippy for all platforms",
checks=[f"clippy_{platform}" for platform in PLATFORMS],
),
Group(
name="unit_tests",
doc="Runs unit tests for all platforms",
checks=[f"crosvm_unit_tests_{platform}" for platform in PLATFORMS],
),
Group(
name="format",
doc="Runs all formatting checks (or fixes)",
checks=[
"rust_format",
"markdown_format",
"python_format",
],
),
Group(
name="default_health_checks",
doc="Health checks to run by default",
checks=[
# Check if lockfiles need updating first. Otherwise another step may do the update.
"rust_lockfiles",
"copyright_header",
"file_ends_with_newline",
"line_endings",
"markdown_format",
"mdbook",
"cargo_doc",
"python_format",
"python_types",
"rust_features",
"rust_format",
],
),
# The groups below are used by builders in CI:
Group(
name="health_checks",
doc="Checks run on the health_check builder",
checks=[
"default_health_checks",
"doc_tests",
"python_tests",
]
+ (["infra_configs", "infra_tests"] if not is_kiwi_repo() else []),
),
*(
Group(
name=f"linux_{platform}",
doc=f"Checks run on the linux-{platform} builder",
checks=[
f"crosvm_tests_{platform}",
f"clippy_{platform}",
f"crosvm_build_default_{platform}",
]
# TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
+ ([f"crosvm_build_no_default_{platform}"] if platform != "mingw64" else []),
)
for platform in PLATFORMS
),
]
# Turn both lists into dicts for convenience
CHECKS_DICT = dict((c.name, c) for c in CHECKS)
GROUPS_DICT = dict((c.name, c) for c in GROUPS)
def validate_config():
"Validates the CHECKS and GROUPS configuration."
for group in GROUPS:
for check in group.checks:
if check not in CHECKS_DICT and check not in GROUPS_DICT:
raise Exception(f"Group {group.name} includes non-existing item {check}.")
def find_in_group(check: Check):
for group in GROUPS:
if check.name in group.checks:
return True
return False
for check in CHECKS:
if not find_in_group(check):
raise Exception(f"Check {check.name} is not included in any group.")
all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS]
for name in all_names:
if all_names.count(name) > 1:
raise Exception(f"Check or group {name} is defined multiple times.")
def get_check_names_in_group(group: Group) -> Generator[str, None, None]:
for name in group.checks:
if name in GROUPS_DICT:
yield from get_check_names_in_group(GROUPS_DICT[name])
else:
yield name
@argh.arg("--list-checks", default=False, help="List names of available checks and exit.")
@argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.")
@argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.")
@argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.")
@argh.arg("--nightly-fmt", default=False, help="Use nightly rust for rustfmt")
@argh.arg(
"checks_or_groups",
help="List of checks or groups to run. Defaults to run the `default` group.",
)
def main(
list_checks: bool = False,
fix: bool = False,
no_delta: bool = False,
no_parallel: bool = False,
nightly_fmt: bool = False,
*checks_or_groups: str,
):
chdir(CROSVM_ROOT)
validate_config()
if not checks_or_groups:
checks_or_groups = ("default",)
# Resolve and validate the groups and checks provided
check_names: List[str] = []
for check_or_group in checks_or_groups:
if check_or_group in CHECKS_DICT:
check_names.append(check_or_group)
elif check_or_group in GROUPS_DICT:
check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group]))
else:
raise Exception(f"No such check or group: {check_or_group}")
# Remove duplicates while preserving order
check_names = list(dict.fromkeys(check_names))
if list_checks:
for check in check_names:
print(check)
return
check_list = [CHECKS_DICT[name] for name in check_names]
run_checks(
check_list,
fix=fix,
run_on_all_files=no_delta,
nightly_fmt=nightly_fmt,
parallel=not no_parallel,
)
def usage():
groups = "\n".join(f" {group.name}: {group.doc}" for group in GROUPS)
checks = "\n".join(f" {check.name}: {check.doc}" for check in CHECKS)
return f"""\
Runs checks on the crosvm codebase.
Basic usage, to run a default selection of checks:
./tools/presubmit
Some checkers can fix issues they find (e.g. formatters, clippy, etc):
./tools/presubmit --fix
Various groups of presubmit checks can be run via:
./tools/presubmit group_name
Available groups are:
{groups}
You can also provide the names of specific checks to run:
./tools/presubmit check1 check2
Available checks are:
{checks}
"""
if __name__ == "__main__":
run_main(main, usage=usage())