Files
2026-07-13 13:12:00 +08:00

124 lines
3.4 KiB
Python

"""Structured progress events and sinks for bench runs."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
from tests.harness_bench.verdict import Verdict
@dataclass(frozen=True)
class HarnessStarted:
"""A harness began provisioning its transport."""
harness: str
transport: str
model: str
@dataclass(frozen=True)
class HarnessSkipped:
"""A harness was skipped whole (unavailable, or provisioning failed).
``transport`` is the resolved transport the skip applies to (``None`` when
it could not be resolved), so a live row can still be labelled with it.
"""
harness: str
reason: str
transport: str | None = None
@dataclass(frozen=True)
class ProbeStarted:
"""A probe began running against a harness."""
harness: str
probe: str
title: str
@dataclass(frozen=True)
class ProbeFinished:
"""A probe produced a verdict (or was skipped as a prerequisite casualty)."""
harness: str
probe: str
title: str
verdict: Verdict
note: str = ""
@dataclass(frozen=True)
class HarnessFinished:
"""A harness completed all its probes."""
harness: str
BenchEvent = HarnessStarted | HarnessSkipped | ProbeStarted | ProbeFinished | HarnessFinished
@runtime_checkable
class ProgressSink(Protocol):
"""Consumes :data:`BenchEvent`\\ s as a bench run emits them.
``emit`` is called from the orchestrator (possibly from several concurrent
harness tasks under ``--jobs`` > 1), so a sink that mutates shared state
must tolerate interleaved calls. The built-in sinks are called on one
event loop thread, so no locking is needed there.
"""
def emit(self, event: BenchEvent) -> None:
"""Handle one event."""
def close(self) -> None:
"""Finalize (flush a live display, etc.). Called once at run end."""
class LineSink:
"""A :class:`ProgressSink` that writes the plain per-probe status lines.
This is the default, TTY-agnostic renderer — the same output the bench
emitted before structured events existed, so a piped or CI run is
unchanged. ``write`` defaults to stderr (the report goes to stdout).
"""
# Per-line progress does not paint the grid, so the stdout report still
# prints it in full (see the rich sink's ``drew_grid = True``).
drew_grid = False
def __init__(self, write) -> None: # write: Callable[[str], None]
self._write = write
def emit(self, event: BenchEvent) -> None:
if isinstance(event, HarnessStarted):
self._write(
f"[{event.harness}] provisioning {event.transport} transport "
f"(model={event.model}); first turn may take ~10-30s..."
)
elif isinstance(event, HarnessSkipped):
self._write(f"[{event.harness}] skipped: {event.reason}")
elif isinstance(event, ProbeStarted):
self._write(f"[{event.harness}] {event.title}: running...")
elif isinstance(event, ProbeFinished):
suffix = f" ({event.note})" if event.note else ""
self._write(f"[{event.harness}] {event.title}: {event.verdict.name}{suffix}")
# HarnessFinished is silent in line mode (the per-probe lines suffice).
def close(self) -> None:
pass
__all__ = [
"BenchEvent",
"HarnessFinished",
"HarnessSkipped",
"HarnessStarted",
"LineSink",
"ProbeFinished",
"ProbeStarted",
"ProgressSink",
]