crosvm/tools/impl/health_check.py
Dennis Kempin 92f804e3f8 tools/health-check: Revamp script to run on git delta only
The updated health-check will by default only run on modified
files. If you do not make changes to python code, python checks
won't run, etc.

The script also simplifies the writing of those checks so we can
start adding more of them.

Luci will be updated to make use of the --list-checks function to
run each check in a separate luci step. In the meantime, we
keep a compatibility layer to translate the old arguments
to the new style.

BUG=b:239255137
TEST=./tools/health-check in all it's variations

Change-Id: I21b986b46c7cfccf3d13f4c76bbd3d0ec7240c26
Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/3827174
Tested-by: Dennis Kempin <denniskempin@google.com>
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
Commit-Queue: Dennis Kempin <denniskempin@google.com>
2022-08-15 18:52:02 +00:00

148 lines
4.2 KiB
Python

#!/usr/bin/env python3
# Copyright 2022 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from fnmatch import fnmatch
from time import time
from typing import Callable, List, NamedTuple
from pathlib import Path
from impl.common import all_tracked_files, cmd, verbose
from dataclasses import dataclass
git = cmd("git")
@dataclass
class CheckContext(object):
"Information passed to each check when it's called."
# Whether or not --fix was set and checks should attempt to fix problems they encounter.
fix: bool
# Use rust nightly version for rust checks
nightly: bool
# All files that this check should cover (e.g. all python files on a python check).
all_files: List[Path]
# Those files of all_files that were modified locally.
modified_files: List[Path]
class Check(NamedTuple):
"Metadata for each check, definining on which files it should run."
# Function to call for this check
check_function: Callable[[CheckContext], None]
# List of globs that this check should be triggered on
files: List[str] = []
python_tools: bool = False
# List of globs to exclude from this check
exclude: List[str] = []
@property
def name(self):
name = self.check_function.__name__
if name.startswith("check_"):
return name[len("check_") :]
return name
def list_modified_files():
"""
Lists files there were modified compared to the upstream branch.
Falls back to all files tracked by git if there is no upstream branch.
"""
upstream = git("rev-parse @{u}").stdout(check=False)
if upstream:
return (Path(f) for f in git("diff --name-only", upstream).lines())
else:
print("WARNING: Not tracking a branch. Checking all files.")
return all_tracked_files()
def should_run_check_on_file(check: Check, file: Path):
"Returns true if `file` should be run on `check`."
# Skip third_party
if str(file).startswith("third_party"):
return False
# Skip excluded files
for glob in check.exclude:
if fnmatch(str(file), glob):
return False
# Match python tools (no file-extension, but with a python shebang line)
if check.python_tools:
if fnmatch(str(file), "tools/*") and file.suffix == "" and file.is_file():
if file.open(errors="ignore").read(32).startswith("#!/usr/bin/env python3"):
return True
# If no constraint is specified, match all files.
if not check.files and not check.python_tools:
return True
# Otherwise, match only those specified by `files`.
for glob in check.files:
if fnmatch(str(file), glob):
return True
return False
def run_check(check: Check, context: CheckContext):
"Runs `check` using the information in `context`. Prints status updates."
start_time = time()
if verbose():
print(f"Checking {check.name}...")
try:
check.check_function(context)
success = True
except Exception as e:
print(e)
success = False
duration = time() - start_time
print(f"Check {check.name}", "OK" if success else "FAILED", f" ({duration:.2f} s)")
return success
def run_checks(
checks_list: List[Check],
fix: bool,
run_on_all_files: bool,
nightly: bool,
):
"""
Runs all checks in checks_list.
Arguments:
fix: Tell checks to fix issues if they can (e.g. run formatter).
run_on_all_files: Do not use git delta, but run on all files.
nightly: Use nightly version of rust tooling.
"""
all_files = [*all_tracked_files()]
if run_on_all_files:
modified_files = all_files
else:
modified_files = [*list_modified_files()]
success = True
for check in checks_list:
context = CheckContext(
fix=fix,
nightly=nightly,
all_files=[f for f in all_files if should_run_check_on_file(check, f)],
modified_files=[f for f in modified_files if should_run_check_on_file(check, f)],
)
if context.modified_files:
if not run_check(check, context):
success = False
return success