• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2022 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import typing
7from typing import Generator, List, Literal, Optional, Tuple
8
9from impl.common import (
10    CROSVM_ROOT,
11    TOOLS_ROOT,
12    Triple,
13    argh,
14    chdir,
15    cmd,
16    is_kiwi_repo,
17    run_main,
18)
19from impl.presubmit import Check, CheckContext, run_checks, Group
20
21python = cmd("python3")
22mypy = cmd("mypy").with_color_env("MYPY_FORCE_COLOR")
23black = cmd("black").with_color_arg(always="--color", never="--no-color")
24mdformat = cmd("mdformat")
25lucicfg = cmd("third_party/depot_tools/lucicfg")
26
27# All supported platforms as a type and a list.
28Platform = Literal["x86_64", "aarch64", "mingw64", "armhf"]
29PLATFORMS: Tuple[Platform, ...] = typing.get_args(Platform)
30
31
32def platform_is_supported(platform: Platform):
33    "Returns true if the platform is available as a target in rustup."
34    triple = Triple.from_shorthand(platform)
35    installed_toolchains = cmd("rustup target list --installed").lines()
36    return str(triple) in installed_toolchains
37
38
39####################################################################################################
40# Check methods
41#
42# Each check returns a Command (or list of Commands) to be run to execute the check. They are
43# registered and configured in the CHECKS list below.
44#
45# Some check functions are factory functions that return a check command for all supported
46# platforms.
47
48
49def check_python_tests(_: CheckContext):
50    "Runs unit tests for python dev tooling."
51    PYTHON_TESTS = [
52        "tests.cl_tests",
53        "impl.common",
54    ]
55    return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS]
56
57
58def check_python_types(context: CheckContext):
59    "Run mypy type checks on python dev tooling."
60    return [mypy("--pretty", file) for file in context.all_files]
61
62
63def check_python_format(context: CheckContext):
64    "Runs the black formatter on python dev tooling."
65    return black.with_args(
66        "--check" if not context.fix else None,
67        *context.modified_files,
68    )
69
70
71def check_markdown_format(context: CheckContext):
72    "Runs mdformat on all markdown files."
73    if "blaze" in mdformat("--version").stdout():
74        raise Exception(
75            "You are using google's mdformat. "
76            + "Please update your PATH to ensure the pip installed mdformat is available."
77        )
78    return mdformat.with_args(
79        "--wrap 100",
80        "--check" if not context.fix else "",
81        *context.modified_files,
82    )
83
84
85def check_rust_format(context: CheckContext):
86    "Runs rustfmt on all modified files."
87    if context.nightly_fmt:
88        rustfmt = cmd(
89            cmd("rustup +nightly which rustfmt"),
90            "--config imports_granularity=item,group_imports=StdExternalCrate",
91        )
92    else:
93        rustfmt = cmd(cmd("rustup which rustfmt"))
94    return rustfmt.with_color_flag().with_args(
95        "--check" if not context.fix else "",
96        *context.modified_files,
97    )
98
99
100def check_cargo_doc(_: CheckContext):
101    "Runs cargo-doc and verifies that no warnings are emitted."
102    return cmd("./tools/cargo-doc").with_env("RUSTDOCFLAGS", "-D warnings").with_color_flag()
103
104
105def check_mdbook(_: CheckContext):
106    "Runs cargo-doc and verifies that no warnings are emitted."
107    return cmd("mdbook build docs/book/")
108
109
110def check_crosvm_tests(platform: Platform):
111    def check(_: CheckContext):
112        if not platform_is_supported(platform):
113            return None
114        dut = None
115        if platform == "x86_64":
116            dut = "--dut=host"
117        elif platform == "aarch64":
118            dut = "--dut=vm"
119        return cmd("./tools/run_tests --platform", platform, dut).with_color_flag()
120
121    check.__doc__ = f"Runs all crosvm tests for {platform}."
122
123    return check
124
125
126def check_crosvm_unit_tests(platform: Platform):
127    def check(_: CheckContext):
128        if not platform_is_supported(platform):
129            return None
130        return cmd("./tools/run_tests --platform", platform).with_color_flag()
131
132    check.__doc__ = f"Runs crosvm unit tests for {platform}."
133
134    return check
135
136
137def check_crosvm_build(platform: Platform, features: Optional[str]):
138    def check(_: CheckContext):
139        return cmd(
140            "./tools/run_tests --no-run --platform",
141            platform,
142            f"--features={features}" if features is not None else None,
143        ).with_color_flag()
144
145    check.__doc__ = f"Builds crosvm for {platform} with features {features}."
146
147    return check
148
149
150def check_clippy(platform: Platform):
151    def check(context: CheckContext):
152        if not platform_is_supported(platform):
153            return None
154        return cmd(
155            "./tools/clippy --platform",
156            platform,
157            "--fix" if context.fix else None,
158        ).with_color_flag()
159
160    check.__doc__ = f"Runs clippy for {platform}."
161
162    return check
163
164
165def check_infra_configs(context: CheckContext):
166    "Validate luci configs by sending them to luci-config."
167    # TODO: Validate config files. Requires authentication with luci inside docker.
168    return [lucicfg("fmt --dry-run", file) for file in context.modified_files]
169
170
171def check_infra_tests(_: CheckContext):
172    "Run recipe.py tests, all of them, regardless of which files were modified."
173    recipes = cmd("infra/recipes.py").with_path_env("third_party/depot_tools")
174    return recipes("test run")
175
176
177def custom_check(name: str, can_fix: bool = False):
178    "Custom checks are written in python in tools/custom_checks. This is a wrapper to call them."
179
180    def check(context: CheckContext):
181        return cmd(
182            TOOLS_ROOT / "custom_checks",
183            name,
184            *context.modified_files,
185            "--fix" if can_fix and context.fix else None,
186        )
187
188    check.__name__ = name.replace("-", "_")
189    check.__doc__ = f"Runs tools/custom_check {name}"
190    return check
191
192
193####################################################################################################
194# Checks configuration
195#
196# Configures which checks are available and on which files they are run.
197# Check names default to the function name minus the check_ prefix
198
199CHECKS: List[Check] = [
200    Check(
201        check_rust_format,
202        files=["**.rs"],
203        exclude=["system_api/src/bindings/*"],
204        can_fix=True,
205    ),
206    Check(
207        check_mdbook,
208        files=["docs/**/*"],
209    ),
210    Check(
211        check_cargo_doc,
212        files=["**.rs", "**Cargo.toml"],
213        priority=True,
214    ),
215    Check(
216        check_python_tests,
217        files=["tools/**.py"],
218        python_tools=True,
219        priority=True,
220    ),
221    Check(
222        check_python_types,
223        files=["tools/**.py"],
224        exclude=["tools/windows/*"],
225        python_tools=True,
226    ),
227    Check(
228        check_python_format,
229        files=["**.py"],
230        python_tools=True,
231        exclude=["infra/recipes.py"],
232        can_fix=True,
233    ),
234    Check(
235        check_markdown_format,
236        files=["**.md"],
237        exclude=[
238            "infra/README.recipes.md",
239            "docs/book/src/appendix/memory_layout.md",
240        ],
241        can_fix=True,
242    ),
243    Check(
244        check_crosvm_build("x86_64", features="direct"),
245        custom_name=f"crosvm_build_direct",
246        files=["**.rs"],
247        priority=True,
248    ),
249    *(
250        Check(
251            check_crosvm_build(platform, features="default"),
252            custom_name=f"crosvm_build_default_{platform}",
253            files=["**.rs"],
254            priority=True,
255        )
256        for platform in PLATFORMS
257    ),
258    *(
259        Check(
260            check_crosvm_tests(platform),
261            custom_name=f"crosvm_tests_{platform}",
262            files=["**.rs"],
263            priority=True,
264        )
265        for platform in PLATFORMS
266    ),
267    *(
268        Check(
269            check_crosvm_unit_tests(platform),
270            custom_name=f"crosvm_unit_tests_{platform}",
271            files=["**.rs"],
272            priority=True,
273        )
274        for platform in PLATFORMS
275    ),
276    *(
277        Check(
278            check_clippy(platform),
279            custom_name=f"clippy_{platform}",
280            files=["**.rs"],
281            can_fix=True,
282            priority=True,
283        )
284        for platform in PLATFORMS
285    ),
286    Check(
287        custom_check("check-copyright-header"),
288        files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"],
289        exclude=[
290            "infra/recipes.py",
291            "hypervisor/src/whpx/whpx_sys/*.h",
292            "third_party/vmm_vhost/*",
293            "net_sys/src/lib.rs",
294            "system_api/src/bindings/*",
295        ],
296        python_tools=True,
297        can_fix=True,
298    ),
299    Check(
300        custom_check("check-rust-features"),
301        files=["**Cargo.toml"],
302    ),
303    Check(
304        custom_check("check-rust-lockfiles"),
305        files=["**Cargo.toml"],
306    ),
307    Check(
308        custom_check("check-line-endings"),
309    ),
310    Check(
311        custom_check("check-file-ends-with-newline"),
312        exclude=[
313            "**.h264",
314            "**.vp8",
315            "**.vp9",
316            "**.ivf",
317            "**.bin",
318            "**.png",
319            "**.min.js",
320            "**.drawio",
321            "**.json",
322        ],
323    ),
324]
325
326# We disable LUCI infra related tests because kokoro doesn't have internet connectivity that
327# the tests rely on.
328if not is_kiwi_repo():
329    CHECKS.extend(
330        [
331            Check(
332                check_infra_configs,
333                files=["infra/config/**.star"],
334                can_fix=True,
335            ),
336            Check(
337                check_infra_tests,
338                files=["infra/**.py"],
339                can_fix=True,
340            ),
341        ]
342    )
343
344####################################################################################################
345# Group configuration
346#
347# Configures pre-defined groups of checks. Some are configured for CI builders and others
348# are configured for convenience during local development.
349
350GROUPS: List[Group] = [
351    # The default group is run if no check or group is explicitly set
352    Group(
353        name="default",
354        doc="Checks run by default",
355        checks=[
356            "health_checks",
357            # Run only one task per platform to prevent blocking on the build cache.
358            "crosvm_tests_x86_64",
359            "crosvm_unit_tests_aarch64",
360            "crosvm_unit_tests_mingw64",
361            "clippy_armhf",
362        ],
363    ),
364    Group(
365        name="quick",
366        doc="Runs a quick subset of presubmit checks.",
367        checks=[
368            "health_checks",
369            "crosvm_unit_tests_x86_64",
370            "clippy_aarch64",
371        ],
372    ),
373    Group(
374        name="all",
375        doc="Run checks of all builders.",
376        checks=[
377            "health_checks",
378            *(f"linux_{platform}" for platform in PLATFORMS),
379            "linux_x86_64_direct",
380        ],
381    ),
382    # Convenience groups for local usage:
383    Group(
384        name="clippy",
385        doc="Runs clippy for all platforms",
386        checks=[f"clippy_{platform}" for platform in PLATFORMS],
387    ),
388    Group(
389        name="unit_tests",
390        doc="Runs unit tests for all platforms",
391        checks=[f"crosvm_unit_tests_{platform}" for platform in PLATFORMS],
392    ),
393    Group(
394        name="format",
395        doc="Runs all formatting checks (or fixes)",
396        checks=[
397            "rust_format",
398            "markdown_format",
399            "python_format",
400        ],
401    ),
402    # The groups below are used by builders in CI:
403    Group(
404        name="health_checks",
405        doc="Checks run on the health_check builder",
406        checks=[
407            "copyright_header",
408            "file_ends_with_newline",
409            "infra_configs",
410            "infra_tests",
411            "line_endings",
412            "markdown_format",
413            "mdbook",
414            "cargo_doc",
415            "python_format",
416            "python_tests",
417            "python_types",
418            "rust_features",
419            "rust_format",
420            "rust_lockfiles",
421        ],
422    ),
423    *(
424        Group(
425            name=f"linux_{platform}",
426            doc=f"Checks run on the linux-{platform} builder",
427            checks=[
428                f"crosvm_tests_{platform}",
429                f"clippy_{platform}",
430                f"crosvm_build_default_{platform}",
431            ],
432        )
433        for platform in PLATFORMS
434    ),
435    Group(
436        name=f"linux_x86_64_direct",
437        doc=f"Checks run on the linux_x86_64_direct builder",
438        checks=[
439            "crosvm_build_direct",
440        ],
441    ),
442]
443
444# Turn both lists into dicts for convenience
445CHECKS_DICT = dict((c.name, c) for c in CHECKS)
446GROUPS_DICT = dict((c.name, c) for c in GROUPS)
447
448
449def validate_config():
450    "Validates the CHECKS and GROUPS configuration."
451    for group in GROUPS:
452        for check in group.checks:
453            if check not in CHECKS_DICT and check not in GROUPS_DICT:
454                raise Exception(f"Group {group.name} includes non-existing item {check}.")
455
456    def find_in_group(check: Check):
457        for group in GROUPS:
458            if check.name in group.checks:
459                return True
460        return False
461
462    for check in CHECKS:
463        if not find_in_group(check):
464            raise Exception(f"Check {check.name} is not included in any group.")
465
466    all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS]
467    for name in all_names:
468        if all_names.count(name) > 1:
469            raise Exception(f"Check or group {name} is defined multiple times.")
470
471
472def get_check_names_in_group(group: Group) -> Generator[str, None, None]:
473    for name in group.checks:
474        if name in GROUPS_DICT:
475            yield from get_check_names_in_group(GROUPS_DICT[name])
476        else:
477            yield name
478
479
480@argh.arg("--list-checks", default=False, help="List names of available checks and exit.")
481@argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.")
482@argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.")
483@argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.")
484@argh.arg("--nightly-fmt", default=False, help="Use nightly rust for rustfmt")
485@argh.arg(
486    "checks_or_groups",
487    help="List of checks or groups to run. Defaults to run the `default` group.",
488)
489def main(
490    list_checks: bool = False,
491    fix: bool = False,
492    no_delta: bool = False,
493    no_parallel: bool = False,
494    nightly_fmt: bool = False,
495    *checks_or_groups: str,
496):
497    chdir(CROSVM_ROOT)
498    validate_config()
499
500    if not checks_or_groups:
501        checks_or_groups = ("default",)
502
503    # Resolve and validate the groups and checks provided
504    check_names: List[str] = []
505    for check_or_group in checks_or_groups:
506        if check_or_group in CHECKS_DICT:
507            check_names.append(check_or_group)
508        elif check_or_group in GROUPS_DICT:
509            check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group]))
510        else:
511            raise Exception(f"No such check or group: {check_or_group}")
512
513    # Remove duplicates while preserving order
514    check_names = list(dict.fromkeys(check_names))
515
516    if list_checks:
517        for check in check_names:
518            print(check)
519        return
520
521    check_list = [CHECKS_DICT[name] for name in check_names]
522
523    run_checks(
524        check_list,
525        fix=fix,
526        run_on_all_files=no_delta,
527        nightly_fmt=nightly_fmt,
528        parallel=not no_parallel,
529    )
530
531
532def usage():
533    groups = "\n".join(f"  {group.name}: {group.doc}" for group in GROUPS)
534    checks = "\n".join(f"  {check.name}: {check.doc}" for check in CHECKS)
535    return f"""\
536Runs checks on the crosvm codebase.
537
538Basic usage, to run a default selection of checks:
539
540    ./tools/presubmit
541
542Some checkers can fix issues they find (e.g. formatters, clippy, etc):
543
544    ./tools/presubmit --fix
545
546
547Various groups of presubmit checks can be run via:
548
549    ./tools/presubmit group_name
550
551Available groups are:
552{groups}
553
554You can also provide the names of specific checks to run:
555
556    ./tools/presubmit check1 check2
557
558Available checks are:
559{checks}
560"""
561
562
563if __name__ == "__main__":
564    run_main(main, usage=usage())
565