xref: /aosp_15_r20/external/crosvm/tools/impl/presubmit.py (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1*bb4ee6a4SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*bb4ee6a4SAndroid Build Coastguard Worker# Copyright 2022 The ChromiumOS Authors
3*bb4ee6a4SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*bb4ee6a4SAndroid Build Coastguard Worker# found in the LICENSE file.
5*bb4ee6a4SAndroid Build Coastguard Worker
6*bb4ee6a4SAndroid Build Coastguard Workerimport os
7*bb4ee6a4SAndroid Build Coastguard Workerimport subprocess
8*bb4ee6a4SAndroid Build Coastguard Workerimport sys
9*bb4ee6a4SAndroid Build Coastguard Workerimport traceback
10*bb4ee6a4SAndroid Build Coastguard Workerfrom concurrent.futures import ThreadPoolExecutor
11*bb4ee6a4SAndroid Build Coastguard Workerfrom dataclasses import dataclass
12*bb4ee6a4SAndroid Build Coastguard Workerfrom datetime import datetime, timedelta
13*bb4ee6a4SAndroid Build Coastguard Workerfrom fnmatch import fnmatch
14*bb4ee6a4SAndroid Build Coastguard Workerfrom pathlib import Path
15*bb4ee6a4SAndroid Build Coastguard Workerfrom time import sleep
16*bb4ee6a4SAndroid Build Coastguard Workerfrom typing import Callable, List, Sequence, NamedTuple, Optional, Union
17*bb4ee6a4SAndroid Build Coastguard Worker
18*bb4ee6a4SAndroid Build Coastguard Workerfrom impl.common import (
19*bb4ee6a4SAndroid Build Coastguard Worker    Command,
20*bb4ee6a4SAndroid Build Coastguard Worker    ParallelCommands,
21*bb4ee6a4SAndroid Build Coastguard Worker    all_tracked_files,
22*bb4ee6a4SAndroid Build Coastguard Worker    cmd,
23*bb4ee6a4SAndroid Build Coastguard Worker    console,
24*bb4ee6a4SAndroid Build Coastguard Worker    rich,
25*bb4ee6a4SAndroid Build Coastguard Worker    strip_ansi_escape_sequences,
26*bb4ee6a4SAndroid Build Coastguard Worker    verbose,
27*bb4ee6a4SAndroid Build Coastguard Worker)
28*bb4ee6a4SAndroid Build Coastguard Worker
29*bb4ee6a4SAndroid Build Coastguard Workergit = cmd("git")
30*bb4ee6a4SAndroid Build Coastguard Worker
31*bb4ee6a4SAndroid Build Coastguard Worker
32*bb4ee6a4SAndroid Build Coastguard Worker@dataclass
33*bb4ee6a4SAndroid Build Coastguard Workerclass CheckContext(object):
34*bb4ee6a4SAndroid Build Coastguard Worker    "Information passed to each check when it's called."
35*bb4ee6a4SAndroid Build Coastguard Worker
36*bb4ee6a4SAndroid Build Coastguard Worker    # Whether or not --fix was set and checks should attempt to fix problems they encounter.
37*bb4ee6a4SAndroid Build Coastguard Worker    fix: bool
38*bb4ee6a4SAndroid Build Coastguard Worker
39*bb4ee6a4SAndroid Build Coastguard Worker    # All files that this check should cover (e.g. all python files on a python check).
40*bb4ee6a4SAndroid Build Coastguard Worker    all_files: List[Path]
41*bb4ee6a4SAndroid Build Coastguard Worker
42*bb4ee6a4SAndroid Build Coastguard Worker    # Those files of all_files that were modified locally.
43*bb4ee6a4SAndroid Build Coastguard Worker    modified_files: List[Path]
44*bb4ee6a4SAndroid Build Coastguard Worker
45*bb4ee6a4SAndroid Build Coastguard Worker    # Files that do not exist upstream and have been added locally.
46*bb4ee6a4SAndroid Build Coastguard Worker    new_files: List[Path]
47*bb4ee6a4SAndroid Build Coastguard Worker
48*bb4ee6a4SAndroid Build Coastguard Worker
49*bb4ee6a4SAndroid Build Coastguard Workerclass Check(NamedTuple):
50*bb4ee6a4SAndroid Build Coastguard Worker    "Metadata for each check, definining on which files it should run."
51*bb4ee6a4SAndroid Build Coastguard Worker
52*bb4ee6a4SAndroid Build Coastguard Worker    # Function to call for this check
53*bb4ee6a4SAndroid Build Coastguard Worker    check_function: Callable[[CheckContext], Union[Command, None, List[Command]]]
54*bb4ee6a4SAndroid Build Coastguard Worker
55*bb4ee6a4SAndroid Build Coastguard Worker    custom_name: Optional[str] = None
56*bb4ee6a4SAndroid Build Coastguard Worker
57*bb4ee6a4SAndroid Build Coastguard Worker    # List of globs that this check should be triggered on
58*bb4ee6a4SAndroid Build Coastguard Worker    files: List[str] = []
59*bb4ee6a4SAndroid Build Coastguard Worker
60*bb4ee6a4SAndroid Build Coastguard Worker    python_tools: bool = False
61*bb4ee6a4SAndroid Build Coastguard Worker
62*bb4ee6a4SAndroid Build Coastguard Worker    # List of globs to exclude from this check
63*bb4ee6a4SAndroid Build Coastguard Worker    exclude: List[str] = []
64*bb4ee6a4SAndroid Build Coastguard Worker
65*bb4ee6a4SAndroid Build Coastguard Worker    # Whether or not this check can fix issues.
66*bb4ee6a4SAndroid Build Coastguard Worker    can_fix: bool = False
67*bb4ee6a4SAndroid Build Coastguard Worker
68*bb4ee6a4SAndroid Build Coastguard Worker    # Which groups this check belongs to.
69*bb4ee6a4SAndroid Build Coastguard Worker    groups: List[str] = []
70*bb4ee6a4SAndroid Build Coastguard Worker
71*bb4ee6a4SAndroid Build Coastguard Worker    # Priority tasks usually take lonkger and are started first, and will show preliminary output.
72*bb4ee6a4SAndroid Build Coastguard Worker    priority: bool = False
73*bb4ee6a4SAndroid Build Coastguard Worker
74*bb4ee6a4SAndroid Build Coastguard Worker    @property
75*bb4ee6a4SAndroid Build Coastguard Worker    def name(self):
76*bb4ee6a4SAndroid Build Coastguard Worker        if self.custom_name:
77*bb4ee6a4SAndroid Build Coastguard Worker            return self.custom_name
78*bb4ee6a4SAndroid Build Coastguard Worker        name = self.check_function.__name__
79*bb4ee6a4SAndroid Build Coastguard Worker        if name.startswith("check_"):
80*bb4ee6a4SAndroid Build Coastguard Worker            return name[len("check_") :]
81*bb4ee6a4SAndroid Build Coastguard Worker        return name
82*bb4ee6a4SAndroid Build Coastguard Worker
83*bb4ee6a4SAndroid Build Coastguard Worker    @property
84*bb4ee6a4SAndroid Build Coastguard Worker    def doc(self):
85*bb4ee6a4SAndroid Build Coastguard Worker        if self.check_function.__doc__:
86*bb4ee6a4SAndroid Build Coastguard Worker            return self.check_function.__doc__.strip()
87*bb4ee6a4SAndroid Build Coastguard Worker        else:
88*bb4ee6a4SAndroid Build Coastguard Worker            return None
89*bb4ee6a4SAndroid Build Coastguard Worker
90*bb4ee6a4SAndroid Build Coastguard Worker
91*bb4ee6a4SAndroid Build Coastguard Workerclass Group(NamedTuple):
92*bb4ee6a4SAndroid Build Coastguard Worker    "Metadata for a group of checks"
93*bb4ee6a4SAndroid Build Coastguard Worker
94*bb4ee6a4SAndroid Build Coastguard Worker    name: str
95*bb4ee6a4SAndroid Build Coastguard Worker
96*bb4ee6a4SAndroid Build Coastguard Worker    doc: str
97*bb4ee6a4SAndroid Build Coastguard Worker
98*bb4ee6a4SAndroid Build Coastguard Worker    checks: List[str]
99*bb4ee6a4SAndroid Build Coastguard Worker
100*bb4ee6a4SAndroid Build Coastguard Worker
101*bb4ee6a4SAndroid Build Coastguard Workerdef list_file_diff():
102*bb4ee6a4SAndroid Build Coastguard Worker    """
103*bb4ee6a4SAndroid Build Coastguard Worker    Lists files there were modified compared to the upstream branch.
104*bb4ee6a4SAndroid Build Coastguard Worker
105*bb4ee6a4SAndroid Build Coastguard Worker    Falls back to all files tracked by git if there is no upstream branch.
106*bb4ee6a4SAndroid Build Coastguard Worker    """
107*bb4ee6a4SAndroid Build Coastguard Worker    upstream = git("rev-parse @{u}").stdout(check=False)
108*bb4ee6a4SAndroid Build Coastguard Worker    if upstream:
109*bb4ee6a4SAndroid Build Coastguard Worker        for line in git("diff --name-status", upstream).lines():
110*bb4ee6a4SAndroid Build Coastguard Worker            parts = line.split("\t", 1)
111*bb4ee6a4SAndroid Build Coastguard Worker            file = Path(parts[1].strip())
112*bb4ee6a4SAndroid Build Coastguard Worker            if file.is_file():
113*bb4ee6a4SAndroid Build Coastguard Worker                yield (parts[0].strip(), file)
114*bb4ee6a4SAndroid Build Coastguard Worker    else:
115*bb4ee6a4SAndroid Build Coastguard Worker        print("WARNING: Not tracking a branch. Checking all files.")
116*bb4ee6a4SAndroid Build Coastguard Worker        for file in all_tracked_files():
117*bb4ee6a4SAndroid Build Coastguard Worker            yield ("M", file)
118*bb4ee6a4SAndroid Build Coastguard Worker
119*bb4ee6a4SAndroid Build Coastguard Worker
120*bb4ee6a4SAndroid Build Coastguard Workerdef should_run_check_on_file(check: Check, file: Path):
121*bb4ee6a4SAndroid Build Coastguard Worker    "Returns true if `file` should be run on `check`."
122*bb4ee6a4SAndroid Build Coastguard Worker
123*bb4ee6a4SAndroid Build Coastguard Worker    # Skip third_party except vmm_vhost.
124*bb4ee6a4SAndroid Build Coastguard Worker    if str(file).startswith("third_party") and not str(file).startswith("third_party/vmm_vhost"):
125*bb4ee6a4SAndroid Build Coastguard Worker        return False
126*bb4ee6a4SAndroid Build Coastguard Worker
127*bb4ee6a4SAndroid Build Coastguard Worker    # Skip excluded files
128*bb4ee6a4SAndroid Build Coastguard Worker    for glob in check.exclude:
129*bb4ee6a4SAndroid Build Coastguard Worker        if fnmatch(str(file), glob):
130*bb4ee6a4SAndroid Build Coastguard Worker            return False
131*bb4ee6a4SAndroid Build Coastguard Worker
132*bb4ee6a4SAndroid Build Coastguard Worker    # Match python tools (no file-extension, but with a python shebang line)
133*bb4ee6a4SAndroid Build Coastguard Worker    if check.python_tools:
134*bb4ee6a4SAndroid Build Coastguard Worker        if fnmatch(str(file), "tools/*") and file.suffix == "" and file.is_file():
135*bb4ee6a4SAndroid Build Coastguard Worker            if file.open(errors="ignore").read(32).startswith("#!/usr/bin/env python3"):
136*bb4ee6a4SAndroid Build Coastguard Worker                return True
137*bb4ee6a4SAndroid Build Coastguard Worker
138*bb4ee6a4SAndroid Build Coastguard Worker    # If no constraint is specified, match all files.
139*bb4ee6a4SAndroid Build Coastguard Worker    if not check.files and not check.python_tools:
140*bb4ee6a4SAndroid Build Coastguard Worker        return True
141*bb4ee6a4SAndroid Build Coastguard Worker
142*bb4ee6a4SAndroid Build Coastguard Worker    # Otherwise, match only those specified by `files`.
143*bb4ee6a4SAndroid Build Coastguard Worker    for glob in check.files:
144*bb4ee6a4SAndroid Build Coastguard Worker        if fnmatch(str(file), glob):
145*bb4ee6a4SAndroid Build Coastguard Worker            return True
146*bb4ee6a4SAndroid Build Coastguard Worker
147*bb4ee6a4SAndroid Build Coastguard Worker    return False
148*bb4ee6a4SAndroid Build Coastguard Worker
149*bb4ee6a4SAndroid Build Coastguard Worker
150*bb4ee6a4SAndroid Build Coastguard Workerclass Task(object):
151*bb4ee6a4SAndroid Build Coastguard Worker    """
152*bb4ee6a4SAndroid Build Coastguard Worker    Represents a task that needs to be executed to perform a `Check`.
153*bb4ee6a4SAndroid Build Coastguard Worker
154*bb4ee6a4SAndroid Build Coastguard Worker    The task can be executed via `Task.execute`, which will update the state variables with
155*bb4ee6a4SAndroid Build Coastguard Worker    status and progress information.
156*bb4ee6a4SAndroid Build Coastguard Worker
157*bb4ee6a4SAndroid Build Coastguard Worker    This information can then be rendered from a separate thread via `Task.status_widget()`
158*bb4ee6a4SAndroid Build Coastguard Worker    """
159*bb4ee6a4SAndroid Build Coastguard Worker
160*bb4ee6a4SAndroid Build Coastguard Worker    def __init__(self, title: str, commands: Sequence[Command], priority: bool):
161*bb4ee6a4SAndroid Build Coastguard Worker        "Display title."
162*bb4ee6a4SAndroid Build Coastguard Worker        self.title = title
163*bb4ee6a4SAndroid Build Coastguard Worker        "Commands to execute."
164*bb4ee6a4SAndroid Build Coastguard Worker        self.commands = commands
165*bb4ee6a4SAndroid Build Coastguard Worker        "Task is a priority check."
166*bb4ee6a4SAndroid Build Coastguard Worker        self.priority = priority
167*bb4ee6a4SAndroid Build Coastguard Worker        "List of log lines (stdout+stderr) produced by the task."
168*bb4ee6a4SAndroid Build Coastguard Worker        self.log_lines: List[str] = []
169*bb4ee6a4SAndroid Build Coastguard Worker        "Task was compleded, but may or not have been successful."
170*bb4ee6a4SAndroid Build Coastguard Worker        self.done = False
171*bb4ee6a4SAndroid Build Coastguard Worker        "True if the task completed successfully."
172*bb4ee6a4SAndroid Build Coastguard Worker        self.success = False
173*bb4ee6a4SAndroid Build Coastguard Worker        "Time the task was started."
174*bb4ee6a4SAndroid Build Coastguard Worker        self.start_time = datetime.min
175*bb4ee6a4SAndroid Build Coastguard Worker        "Duration the task took to execute. Only filled after completion."
176*bb4ee6a4SAndroid Build Coastguard Worker        self.duration = timedelta.max
177*bb4ee6a4SAndroid Build Coastguard Worker        "Spinner object for status_widget UI."
178*bb4ee6a4SAndroid Build Coastguard Worker        self.spinner = rich.spinner.Spinner("point", title)
179*bb4ee6a4SAndroid Build Coastguard Worker
180*bb4ee6a4SAndroid Build Coastguard Worker    def status_widget(self):
181*bb4ee6a4SAndroid Build Coastguard Worker        "Returns a rich console object showing the currrent status of the task."
182*bb4ee6a4SAndroid Build Coastguard Worker        duration = self.duration if self.done else datetime.now() - self.start_time
183*bb4ee6a4SAndroid Build Coastguard Worker        title = f"[{duration.total_seconds():6.2f}s] [bold]{self.title}[/bold]"
184*bb4ee6a4SAndroid Build Coastguard Worker
185*bb4ee6a4SAndroid Build Coastguard Worker        if self.done:
186*bb4ee6a4SAndroid Build Coastguard Worker            status: str = "[green]OK [/green]" if self.success else "[red]ERR[/red]"
187*bb4ee6a4SAndroid Build Coastguard Worker            title_widget = rich.text.Text.from_markup(f"{status} {title}")
188*bb4ee6a4SAndroid Build Coastguard Worker        else:
189*bb4ee6a4SAndroid Build Coastguard Worker            self.spinner.text = rich.text.Text.from_markup(title)
190*bb4ee6a4SAndroid Build Coastguard Worker            title_widget = self.spinner
191*bb4ee6a4SAndroid Build Coastguard Worker
192*bb4ee6a4SAndroid Build Coastguard Worker        if not self.priority:
193*bb4ee6a4SAndroid Build Coastguard Worker            return title_widget
194*bb4ee6a4SAndroid Build Coastguard Worker
195*bb4ee6a4SAndroid Build Coastguard Worker        last_lines = [
196*bb4ee6a4SAndroid Build Coastguard Worker            self.log_lines[-3] if len(self.log_lines) >= 3 else "",
197*bb4ee6a4SAndroid Build Coastguard Worker            self.log_lines[-2] if len(self.log_lines) >= 2 else "",
198*bb4ee6a4SAndroid Build Coastguard Worker            self.log_lines[-1] if len(self.log_lines) >= 1 else "",
199*bb4ee6a4SAndroid Build Coastguard Worker        ]
200*bb4ee6a4SAndroid Build Coastguard Worker
201*bb4ee6a4SAndroid Build Coastguard Worker        return rich.console.Group(
202*bb4ee6a4SAndroid Build Coastguard Worker            *(
203*bb4ee6a4SAndroid Build Coastguard Worker                # Print last log lines without it's original colors
204*bb4ee6a4SAndroid Build Coastguard Worker                rich.text.Text(
205*bb4ee6a4SAndroid Build Coastguard Worker                    "│ " + strip_ansi_escape_sequences(log_line),
206*bb4ee6a4SAndroid Build Coastguard Worker                    style="light_slate_grey",
207*bb4ee6a4SAndroid Build Coastguard Worker                    overflow="ellipsis",
208*bb4ee6a4SAndroid Build Coastguard Worker                    no_wrap=True,
209*bb4ee6a4SAndroid Build Coastguard Worker                )
210*bb4ee6a4SAndroid Build Coastguard Worker                for log_line in last_lines
211*bb4ee6a4SAndroid Build Coastguard Worker            ),
212*bb4ee6a4SAndroid Build Coastguard Worker            rich.text.Text("└ ", end="", style="light_slate_grey"),
213*bb4ee6a4SAndroid Build Coastguard Worker            title_widget,
214*bb4ee6a4SAndroid Build Coastguard Worker            rich.text.Text(),
215*bb4ee6a4SAndroid Build Coastguard Worker        )
216*bb4ee6a4SAndroid Build Coastguard Worker
217*bb4ee6a4SAndroid Build Coastguard Worker    def execute(self):
218*bb4ee6a4SAndroid Build Coastguard Worker        "Execute the task while updating the status variables."
219*bb4ee6a4SAndroid Build Coastguard Worker        try:
220*bb4ee6a4SAndroid Build Coastguard Worker            self.start_time = datetime.now()
221*bb4ee6a4SAndroid Build Coastguard Worker            success = True
222*bb4ee6a4SAndroid Build Coastguard Worker            if verbose():
223*bb4ee6a4SAndroid Build Coastguard Worker                for command in self.commands:
224*bb4ee6a4SAndroid Build Coastguard Worker                    self.log_lines.append(f"$ {command}")
225*bb4ee6a4SAndroid Build Coastguard Worker
226*bb4ee6a4SAndroid Build Coastguard Worker            # Spawn all commands as separate processes
227*bb4ee6a4SAndroid Build Coastguard Worker            processes = [
228*bb4ee6a4SAndroid Build Coastguard Worker                command.popen(stdout=subprocess.PIPE, stderr=subprocess.STDOUT, errors="replace")
229*bb4ee6a4SAndroid Build Coastguard Worker                for command in self.commands
230*bb4ee6a4SAndroid Build Coastguard Worker            ]
231*bb4ee6a4SAndroid Build Coastguard Worker
232*bb4ee6a4SAndroid Build Coastguard Worker            # The stdout is collected before we wait for the processes to exit so that the UI is
233*bb4ee6a4SAndroid Build Coastguard Worker            # at least real-time for the first process. Note that in this way, the output for
234*bb4ee6a4SAndroid Build Coastguard Worker            # other processes other than the first process are not real-time. In addition, we
235*bb4ee6a4SAndroid Build Coastguard Worker            # can't proactively kill other processes in the same task if any process fails.
236*bb4ee6a4SAndroid Build Coastguard Worker            for process in processes:
237*bb4ee6a4SAndroid Build Coastguard Worker                assert process.stdout
238*bb4ee6a4SAndroid Build Coastguard Worker                for line in iter(process.stdout.readline, ""):
239*bb4ee6a4SAndroid Build Coastguard Worker                    self.log_lines.append(line.strip())
240*bb4ee6a4SAndroid Build Coastguard Worker
241*bb4ee6a4SAndroid Build Coastguard Worker            # Wait for all processes to finish and check return code
242*bb4ee6a4SAndroid Build Coastguard Worker            for process in processes:
243*bb4ee6a4SAndroid Build Coastguard Worker                if process.wait() != 0:
244*bb4ee6a4SAndroid Build Coastguard Worker                    success = False
245*bb4ee6a4SAndroid Build Coastguard Worker
246*bb4ee6a4SAndroid Build Coastguard Worker            self.duration = datetime.now() - self.start_time
247*bb4ee6a4SAndroid Build Coastguard Worker            self.success = success
248*bb4ee6a4SAndroid Build Coastguard Worker            self.done = True
249*bb4ee6a4SAndroid Build Coastguard Worker        except Exception:
250*bb4ee6a4SAndroid Build Coastguard Worker            self.log_lines.append(traceback.format_exc())
251*bb4ee6a4SAndroid Build Coastguard Worker
252*bb4ee6a4SAndroid Build Coastguard Worker
253*bb4ee6a4SAndroid Build Coastguard Workerdef print_logs(tasks: List[Task]):
254*bb4ee6a4SAndroid Build Coastguard Worker    "Prints logs of all failed or unfinished tasks."
255*bb4ee6a4SAndroid Build Coastguard Worker    for task in tasks:
256*bb4ee6a4SAndroid Build Coastguard Worker        if not task.done:
257*bb4ee6a4SAndroid Build Coastguard Worker            print()
258*bb4ee6a4SAndroid Build Coastguard Worker            console.rule(f"{task.title} did not finish", style="yellow")
259*bb4ee6a4SAndroid Build Coastguard Worker            for line in task.log_lines:
260*bb4ee6a4SAndroid Build Coastguard Worker                print(line)
261*bb4ee6a4SAndroid Build Coastguard Worker            if not task.log_lines:
262*bb4ee6a4SAndroid Build Coastguard Worker                print(f"{task.title} did not output any logs")
263*bb4ee6a4SAndroid Build Coastguard Worker    for task in tasks:
264*bb4ee6a4SAndroid Build Coastguard Worker        if task.done and not task.success:
265*bb4ee6a4SAndroid Build Coastguard Worker            console.rule(f"{task.title} failed", style="red")
266*bb4ee6a4SAndroid Build Coastguard Worker            for line in task.log_lines:
267*bb4ee6a4SAndroid Build Coastguard Worker                print(line)
268*bb4ee6a4SAndroid Build Coastguard Worker            if not task.log_lines:
269*bb4ee6a4SAndroid Build Coastguard Worker                print(f"{task.title} did not output any logs")
270*bb4ee6a4SAndroid Build Coastguard Worker
271*bb4ee6a4SAndroid Build Coastguard Worker
272*bb4ee6a4SAndroid Build Coastguard Workerdef print_summary(tasks: List[Task]):
273*bb4ee6a4SAndroid Build Coastguard Worker    "Prints a summary of all task results."
274*bb4ee6a4SAndroid Build Coastguard Worker    console.rule("Summary")
275*bb4ee6a4SAndroid Build Coastguard Worker    tasks.sort(key=lambda t: t.duration)
276*bb4ee6a4SAndroid Build Coastguard Worker    for task in tasks:
277*bb4ee6a4SAndroid Build Coastguard Worker        title = f"[{task.duration.total_seconds():6.2f}s] [bold]{task.title}[/bold]"
278*bb4ee6a4SAndroid Build Coastguard Worker        status: str = "[green]OK [/green]" if task.success else "[red]ERR[/red]"
279*bb4ee6a4SAndroid Build Coastguard Worker        console.print(f"{status} {title}")
280*bb4ee6a4SAndroid Build Coastguard Worker
281*bb4ee6a4SAndroid Build Coastguard Worker
282*bb4ee6a4SAndroid Build Coastguard Workerdef execute_tasks_parallel(tasks: List[Task]):
283*bb4ee6a4SAndroid Build Coastguard Worker    "Executes the list of tasks in parallel, while rendering live status updates."
284*bb4ee6a4SAndroid Build Coastguard Worker    with ThreadPoolExecutor() as executor:
285*bb4ee6a4SAndroid Build Coastguard Worker        try:
286*bb4ee6a4SAndroid Build Coastguard Worker            # Since tasks are executed in subprocesses, we can use a thread pool to parallelize
287*bb4ee6a4SAndroid Build Coastguard Worker            # despite the GIL.
288*bb4ee6a4SAndroid Build Coastguard Worker            task_futures = [executor.submit(lambda: t.execute()) for t in tasks]
289*bb4ee6a4SAndroid Build Coastguard Worker
290*bb4ee6a4SAndroid Build Coastguard Worker            # Render task updates while they are executing in the background.
291*bb4ee6a4SAndroid Build Coastguard Worker            with rich.live.Live(refresh_per_second=30) as live:
292*bb4ee6a4SAndroid Build Coastguard Worker                while True:
293*bb4ee6a4SAndroid Build Coastguard Worker                    live.update(
294*bb4ee6a4SAndroid Build Coastguard Worker                        rich.console.Group(
295*bb4ee6a4SAndroid Build Coastguard Worker                            *(t.status_widget() for t in tasks),
296*bb4ee6a4SAndroid Build Coastguard Worker                            rich.text.Text(),
297*bb4ee6a4SAndroid Build Coastguard Worker                            rich.text.Text.from_markup(
298*bb4ee6a4SAndroid Build Coastguard Worker                                "[green]Tip:[/green] Press CTRL-C to abort execution and see all logs."
299*bb4ee6a4SAndroid Build Coastguard Worker                            ),
300*bb4ee6a4SAndroid Build Coastguard Worker                        )
301*bb4ee6a4SAndroid Build Coastguard Worker                    )
302*bb4ee6a4SAndroid Build Coastguard Worker                    if all(future.done() for future in task_futures):
303*bb4ee6a4SAndroid Build Coastguard Worker                        break
304*bb4ee6a4SAndroid Build Coastguard Worker                    sleep(0.1)
305*bb4ee6a4SAndroid Build Coastguard Worker        except KeyboardInterrupt:
306*bb4ee6a4SAndroid Build Coastguard Worker            print_logs(tasks)
307*bb4ee6a4SAndroid Build Coastguard Worker            # Force exit to skip waiting for the executor to shutdown. This will kill all
308*bb4ee6a4SAndroid Build Coastguard Worker            # running subprocesses.
309*bb4ee6a4SAndroid Build Coastguard Worker            os._exit(1)  # type: ignore
310*bb4ee6a4SAndroid Build Coastguard Worker
311*bb4ee6a4SAndroid Build Coastguard Worker    # Render error logs and summary after execution
312*bb4ee6a4SAndroid Build Coastguard Worker    print_logs(tasks)
313*bb4ee6a4SAndroid Build Coastguard Worker    print_summary(tasks)
314*bb4ee6a4SAndroid Build Coastguard Worker
315*bb4ee6a4SAndroid Build Coastguard Worker    if any(not t.success for t in tasks):
316*bb4ee6a4SAndroid Build Coastguard Worker        raise Exception("Some checks failed")
317*bb4ee6a4SAndroid Build Coastguard Worker
318*bb4ee6a4SAndroid Build Coastguard Worker
319*bb4ee6a4SAndroid Build Coastguard Workerdef execute_tasks_serial(tasks: List[Task]):
320*bb4ee6a4SAndroid Build Coastguard Worker    "Executes the list of tasks one-by-one"
321*bb4ee6a4SAndroid Build Coastguard Worker    for task in tasks:
322*bb4ee6a4SAndroid Build Coastguard Worker        console.rule(task.title)
323*bb4ee6a4SAndroid Build Coastguard Worker        for command in task.commands:
324*bb4ee6a4SAndroid Build Coastguard Worker            command.fg()
325*bb4ee6a4SAndroid Build Coastguard Worker        console.print()
326*bb4ee6a4SAndroid Build Coastguard Worker
327*bb4ee6a4SAndroid Build Coastguard Worker
328*bb4ee6a4SAndroid Build Coastguard Workerdef generate_plan(
329*bb4ee6a4SAndroid Build Coastguard Worker    checks_list: List[Check],
330*bb4ee6a4SAndroid Build Coastguard Worker    fix: bool,
331*bb4ee6a4SAndroid Build Coastguard Worker    run_on_all_files: bool,
332*bb4ee6a4SAndroid Build Coastguard Worker):
333*bb4ee6a4SAndroid Build Coastguard Worker    "Generates a list of `Task`s to execute the checks provided in `checks_list`"
334*bb4ee6a4SAndroid Build Coastguard Worker    all_files = [*all_tracked_files()]
335*bb4ee6a4SAndroid Build Coastguard Worker    file_diff = [*list_file_diff()]
336*bb4ee6a4SAndroid Build Coastguard Worker    new_files = [f for (s, f) in file_diff if s == "A"]
337*bb4ee6a4SAndroid Build Coastguard Worker    if run_on_all_files:
338*bb4ee6a4SAndroid Build Coastguard Worker        modified_files = all_files
339*bb4ee6a4SAndroid Build Coastguard Worker    else:
340*bb4ee6a4SAndroid Build Coastguard Worker        modified_files = [f for (s, f) in file_diff if s in ("M", "A")]
341*bb4ee6a4SAndroid Build Coastguard Worker    tasks: List[Task] = []
342*bb4ee6a4SAndroid Build Coastguard Worker    unsupported_checks: List[str] = []
343*bb4ee6a4SAndroid Build Coastguard Worker    for check in checks_list:
344*bb4ee6a4SAndroid Build Coastguard Worker        if fix and not check.can_fix:
345*bb4ee6a4SAndroid Build Coastguard Worker            continue
346*bb4ee6a4SAndroid Build Coastguard Worker        context = CheckContext(
347*bb4ee6a4SAndroid Build Coastguard Worker            fix=fix,
348*bb4ee6a4SAndroid Build Coastguard Worker            all_files=[f for f in all_files if should_run_check_on_file(check, f)],
349*bb4ee6a4SAndroid Build Coastguard Worker            modified_files=[f for f in modified_files if should_run_check_on_file(check, f)],
350*bb4ee6a4SAndroid Build Coastguard Worker            new_files=[f for f in new_files if should_run_check_on_file(check, f)],
351*bb4ee6a4SAndroid Build Coastguard Worker        )
352*bb4ee6a4SAndroid Build Coastguard Worker        if context.modified_files:
353*bb4ee6a4SAndroid Build Coastguard Worker            maybe_commands = check.check_function(context)
354*bb4ee6a4SAndroid Build Coastguard Worker            if maybe_commands is None:
355*bb4ee6a4SAndroid Build Coastguard Worker                unsupported_checks.append(check.name)
356*bb4ee6a4SAndroid Build Coastguard Worker                continue
357*bb4ee6a4SAndroid Build Coastguard Worker            commands_list = maybe_commands if isinstance(maybe_commands, list) else [maybe_commands]
358*bb4ee6a4SAndroid Build Coastguard Worker            title = f"fixing {check.name}" if fix else check.name
359*bb4ee6a4SAndroid Build Coastguard Worker            tasks.append(Task(title, commands_list, check.priority))
360*bb4ee6a4SAndroid Build Coastguard Worker
361*bb4ee6a4SAndroid Build Coastguard Worker    if unsupported_checks:
362*bb4ee6a4SAndroid Build Coastguard Worker        console.print("[yellow]Warning:[/yellow] The following checks cannot be run:")
363*bb4ee6a4SAndroid Build Coastguard Worker        for unsupported_check in unsupported_checks:
364*bb4ee6a4SAndroid Build Coastguard Worker            console.print(f" - {unsupported_check}")
365*bb4ee6a4SAndroid Build Coastguard Worker        console.print()
366*bb4ee6a4SAndroid Build Coastguard Worker        console.print("[green]Tip:[/green] Use the dev container to run presubmits:")
367*bb4ee6a4SAndroid Build Coastguard Worker        console.print()
368*bb4ee6a4SAndroid Build Coastguard Worker        console.print(
369*bb4ee6a4SAndroid Build Coastguard Worker            f"  [blue] $ tools/dev_container tools/presubmit {' '.join(sys.argv[1:])}[/blue]"
370*bb4ee6a4SAndroid Build Coastguard Worker        )
371*bb4ee6a4SAndroid Build Coastguard Worker        console.print()
372*bb4ee6a4SAndroid Build Coastguard Worker
373*bb4ee6a4SAndroid Build Coastguard Worker    if not os.access("/dev/kvm", os.W_OK):
374*bb4ee6a4SAndroid Build Coastguard Worker        console.print("[yellow]Warning:[/yellow] Cannot access KVM. Integration tests are not run.")
375*bb4ee6a4SAndroid Build Coastguard Worker
376*bb4ee6a4SAndroid Build Coastguard Worker    # Sort so that priority tasks are launched (and rendered) first
377*bb4ee6a4SAndroid Build Coastguard Worker    tasks.sort(key=lambda t: (t.priority, t.title), reverse=True)
378*bb4ee6a4SAndroid Build Coastguard Worker    return tasks
379*bb4ee6a4SAndroid Build Coastguard Worker
380*bb4ee6a4SAndroid Build Coastguard Worker
381*bb4ee6a4SAndroid Build Coastguard Workerdef run_checks(
382*bb4ee6a4SAndroid Build Coastguard Worker    checks_list: List[Check],
383*bb4ee6a4SAndroid Build Coastguard Worker    fix: bool,
384*bb4ee6a4SAndroid Build Coastguard Worker    run_on_all_files: bool,
385*bb4ee6a4SAndroid Build Coastguard Worker    parallel: bool,
386*bb4ee6a4SAndroid Build Coastguard Worker):
387*bb4ee6a4SAndroid Build Coastguard Worker    """
388*bb4ee6a4SAndroid Build Coastguard Worker    Runs all checks in checks_list.
389*bb4ee6a4SAndroid Build Coastguard Worker
390*bb4ee6a4SAndroid Build Coastguard Worker    Arguments:
391*bb4ee6a4SAndroid Build Coastguard Worker        fix: Run fixes instead of checks on `Check`s that support it.
392*bb4ee6a4SAndroid Build Coastguard Worker        run_on_all_files: Do not use git delta, but run on all files.
393*bb4ee6a4SAndroid Build Coastguard Worker        nightly_fmt: Use nightly version of rust tooling.
394*bb4ee6a4SAndroid Build Coastguard Worker        parallel: Run tasks in parallel.
395*bb4ee6a4SAndroid Build Coastguard Worker    """
396*bb4ee6a4SAndroid Build Coastguard Worker    tasks = generate_plan(checks_list, fix, run_on_all_files)
397*bb4ee6a4SAndroid Build Coastguard Worker    if len(tasks) == 1:
398*bb4ee6a4SAndroid Build Coastguard Worker        parallel = False
399*bb4ee6a4SAndroid Build Coastguard Worker
400*bb4ee6a4SAndroid Build Coastguard Worker    if parallel:
401*bb4ee6a4SAndroid Build Coastguard Worker        execute_tasks_parallel(list(tasks))
402*bb4ee6a4SAndroid Build Coastguard Worker    else:
403*bb4ee6a4SAndroid Build Coastguard Worker        execute_tasks_serial(list(tasks))
404