Files
2026-04-09 11:14:05 +02:00

328 lines
9.8 KiB
Python

from __future__ import annotations
from sys import stderr
from contextlib import suppress
import time
from typing import Optional, Sequence
class CargoHackDashboard:
_STATUS_WIDTH = len("SKIPPED (cache hit)")
_RICH_CRATE_WIDTH_CAP = 48
_SECONDS_WIDTH = 6
def __init__(
self,
total_crates: int,
tag: str,
max_crate_name_width: int = 0,
rich_enabled: Optional[bool] = None,
verbose: bool = False,
):
self.total_crates = total_crates
self.tag = tag
self.max_crate_name_width = max_crate_name_width
self.verbose = verbose
self._rich_crate_width = min(max(self.max_crate_name_width, 24), self._RICH_CRATE_WIDTH_CAP)
self.success = 0
self.skipped = 0
self.failed = 0
self.cache_hits = 0
self._start_time = time.monotonic()
self._rich_enabled = self._should_use_rich(rich_enabled)
self._progress = None
self._summary = None
self._live = None
self._workspace_task: Optional[int] = None
self._summary_task: Optional[int] = None
self._console = None
self._active_crate: Optional[tuple[str, int]] = None
if self._rich_enabled:
self._init_rich()
# ----------------------------------------------------------
def elapsed_seconds(self) -> float:
return time.monotonic() - self._start_time
# ----------------------------------------------------------
def _format_results(self) -> str:
return f"ok={self.success} skip={self.skipped} (cache={self.cache_hits}) fail={self.failed}"
# ----------------------------------------------------------
def _format_position(self, index: int) -> str:
width = len(str(self.total_crates))
return f"[{index:>{width}}/{self.total_crates}]"
# ----------------------------------------------------------
def _format_status(self, status: str) -> str:
return f"{status:<{self._STATUS_WIDTH}}"
# ----------------------------------------------------------
def _format_crate_name(self, crate_name: str) -> str:
width = max(self.max_crate_name_width, len(crate_name))
return f"{crate_name:<{width}}"
# ----------------------------------------------------------
def _format_rich_crate_name(self, crate_name: str) -> str:
if len(crate_name) > self._rich_crate_width:
return f"{crate_name[:self._rich_crate_width - 3]}..."
return f"{crate_name:<{self._rich_crate_width}}"
# ----------------------------------------------------------
def _format_seconds(self, seconds: float) -> str:
return f"{seconds:{self._SECONDS_WIDTH}.1f}s"
# ----------------------------------------------------------
def _should_use_rich(self, rich_enabled: Optional[bool]) -> bool:
if rich_enabled is not None:
return rich_enabled
if not stderr.isatty():
return False
with suppress(Exception):
from rich.console import Console
return Console(file=stderr).is_terminal
return False
# ----------------------------------------------------------
def _init_rich(self):
from rich.console import Console
from rich.live import Live
from rich.progress import (
Progress,
SpinnerColumn,
BarColumn,
TextColumn,
TimeElapsedColumn,
)
from rich.console import Group
self._console = Console(
file=stderr,
force_terminal=True,
force_interactive=True,
)
self._progress = Progress(
SpinnerColumn(),
BarColumn(),
TextColumn("{task.completed}/{task.total}"),
TimeElapsedColumn(),
TextColumn("|"),
TextColumn("{task.description}"),
console=self._console,
refresh_per_second=12,
)
self._summary = Progress(
TextColumn("{task.description}"),
console=self._console,
refresh_per_second=12,
)
self._live = Live(
Group(self._progress, self._summary),
console=self._console,
refresh_per_second=12,
)
self._live.start()
self._workspace_task = self._progress.add_task(
"",
total=self.total_crates,
)
self._summary_task = self._summary.add_task("")
# ----------------------------------------------------------
def log(self, message: str):
if self._rich_enabled:
self._console.print(message)
else:
print(message, file=stderr)
# ----------------------------------------------------------
def log_crate_detail(self, crate_name: str, message: str):
if self._rich_enabled:
return
if self._active_crate and self._active_crate[0] == crate_name:
_, index = self._active_crate
self.log(f"{self.tag} {self._format_position(index)} {message}")
return
self.log(f"{self.tag} ({crate_name}) {message}")
# ----------------------------------------------------------
def log_cache_invalidation_summary(self, removed: int, missing: int):
parts = [f"{removed} removed", f"{missing} already missing"]
self.log(f" - dependent cache invalidation: {', '.join(parts)}.")
# ----------------------------------------------------------
def close(self):
if self._rich_enabled and self._live:
self._live.stop()
# ----------------------------------------------------------
def start_crate(self, crate_name: str, index: int):
self._active_crate = (crate_name, index)
if not self._rich_enabled:
print(
f"{self.tag} {self._format_position(index)} Checking {crate_name}",
file=stderr,
)
return
self._progress.update(
self._workspace_task,
description=f"Checking: {self._format_rich_crate_name(crate_name)}",
)
self._summary.update(
self._summary_task,
description=f"Progress: {self._format_results()}",
)
# ----------------------------------------------------------
def finish_crate(
self,
*,
crate_name: str,
index: int,
skipped: bool,
success: bool,
crate_elapsed: float,
):
status = "OK"
if skipped:
self.skipped += 1
self.cache_hits += 1
status = "SKIPPED (cache hit)"
elif success:
self.success += 1
status = "OK"
else:
self.failed += 1
status = "FAILED"
total_elapsed = self.elapsed_seconds()
if not self._rich_enabled:
print(
f"{self.tag} {self._format_position(index)} {self._format_status(status)} "
f"(crate {self._format_seconds(crate_elapsed)}, total {self._format_seconds(total_elapsed)})",
file=stderr,
)
self._active_crate = None
return
message = (
f"{self.tag} {self._format_position(index)} {self._format_crate_name(crate_name)}: "
f"{self._format_status(status)} "
f"(crate {self._format_seconds(crate_elapsed)}, total {self._format_seconds(total_elapsed)})"
)
self._progress.advance(self._workspace_task)
self._summary.update(
self._summary_task,
description=f"Progress: {self._format_results()}",
)
self.log(message)
self._active_crate = None
# ----------------------------------------------------------
def fail(self, crate_name: str):
if not self._rich_enabled:
return
elapsed = self.elapsed_seconds()
self._progress.update(
self._workspace_task,
description=f"Failed at {crate_name} after {elapsed:.1f}s",
)
self._summary.update(
self._summary_task,
description=f"Progress: {self._format_results()}",
)
# ----------------------------------------------------------
def interrupt(self):
if not self._rich_enabled:
return
elapsed = self.elapsed_seconds()
self._progress.update(
self._workspace_task,
description=f"Interrupted by user after {elapsed:.1f}s",
)
self._summary.update(
self._summary_task,
description=f"Progress: {self._format_results()}",
)
# ----------------------------------------------------------
def finish(self):
if not self._rich_enabled:
return
elapsed = self.elapsed_seconds()
self._progress.update(
self._workspace_task,
description=f"Done in {elapsed:.1f}s",
)
self._summary.update(
self._summary_task,
description=f"Progress: {self._format_results()}",
)
# ----------------------------------------------------------
def print_summary(
self,
failed_crates: Sequence[str],
*,
interrupted: bool = False,
stopped_early: bool = False,
):
elapsed = self.elapsed_seconds()
status = "INTERRUPTED" if interrupted else "FAILED" if failed_crates else "OK"
self.log(f"{self.tag} Summary ({status})")
self.log(f"{self.tag} Elapsed: {elapsed:.1f}s")
self.log(f"{self.tag} Results: {self._format_results()}")
if interrupted:
self.log(f"{self.tag} Interrupted by user.")
elif stopped_early:
self.log(f"{self.tag} Stopped at first failure.")
if failed_crates:
self.log(f"{self.tag} Failed crates:")
for crate in failed_crates:
self.log(f" - {crate}")