mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 17:41:11 +00:00
328 lines
9.8 KiB
Python
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}")
|