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

385 lines
14 KiB
Python

"""The bench orchestrator: run probes across harnesses into a matrix.
Probes *within* a harness are sequential — they share one driver/session with
a single in-flight turn per conversation. Harnesses run one at a time by
default (``jobs=1``); ``jobs>1`` runs up to N concurrently, bounded by a
semaphore, to cut wall-clock while keeping process/gateway load capped. Under a
parallel run, full-server harnesses share one server+runner (see
:func:`_maybe_shared_full_server`) rather than each booting their own.
"""
from __future__ import annotations
import asyncio
import contextlib
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from tests.harness_bench.driver import ProvisioningError
from tests.harness_bench.events import (
HarnessFinished,
HarnessSkipped,
HarnessStarted,
LineSink,
ProbeFinished,
ProbeStarted,
ProgressSink,
)
from tests.harness_bench.full_server import SharedFullServer
from tests.harness_bench.probes import ALL_PROBES, CapabilityProbe
from tests.harness_bench.profile import BenchProfile
from tests.harness_bench.runtime_env import bench_creds_skip_reason, resolve_bench_env
from tests.harness_bench.transport import resolve_driver_class, resolve_transport_name
from tests.harness_bench.verdict import Applicability, Priority, ProbeResult, Verdict, reconcile
_logger = logging.getLogger(__name__)
# Backward-compatible line callback; new callers should use ProgressSink.
Progress = Callable[[str], None]
_PREREQ_PROBE = "basic_turn"
@dataclass(frozen=True)
class CellResult:
"""One dimension's outcome for one harness (a matrix cell).
:param observed: The raw verdict the probe produced.
:param declared: The verdict the profile claims.
:param verdict: The reconciled verdict — equals *observed* unless the
two concrete facts disagree, in which case ``DRIFT``.
"""
probe_name: str
title: str
priority: Priority
observed: Verdict
declared: Verdict
verdict: Verdict
note: str = ""
detail: dict = field(default_factory=dict)
@property
def is_drift(self) -> bool:
return self.verdict is Verdict.DRIFT
@dataclass(frozen=True)
class HarnessReport:
"""Every cell for one harness, plus a whole-harness skip reason.
:param transport: The transport that actually ran this harness (the
*resolved* driver, e.g. ``full-server`` for an SDK harness on the
default), which can differ from ``profile.transport`` — that field is
the harness *family* marker, not the effective driver.
"""
profile: BenchProfile
cells: list[CellResult]
skipped_reason: str | None = None
transport: str | None = None
@property
def has_drift(self) -> bool:
return any(c.is_drift for c in self.cells)
@dataclass(frozen=True)
class BenchMatrix:
"""The full run: one :class:`HarnessReport` per harness."""
reports: list[HarnessReport]
@property
def has_drift(self) -> bool:
return any(r.has_drift for r in self.reports)
def _is_native(profile: BenchProfile) -> bool:
"""Whether *profile* names a native harness (drives the applicability gate)."""
return profile.transport not in {"sdk-inproc"}
def _applicable(probe: CapabilityProbe, profile: BenchProfile) -> bool:
if probe.applies_to is Applicability.BOTH:
return True
if probe.applies_to is Applicability.NATIVE:
return _is_native(profile)
return not _is_native(profile)
def _cell(probe: CapabilityProbe, profile: BenchProfile, observed: ProbeResult) -> CellResult:
declared = profile.declared_for(probe.name)
return CellResult(
probe_name=probe.name,
title=probe.title,
priority=probe.priority,
observed=observed.verdict,
declared=declared,
verdict=reconcile(observed.verdict, declared),
note=observed.note,
detail=observed.detail,
)
def _uniform_report(
profile: BenchProfile,
probes: list[CapabilityProbe],
observed: ProbeResult,
*,
skipped_reason: str | None = None,
transport: str | None = None,
) -> HarnessReport:
"""A report where every applicable probe shares one *observed* result.
Used for the offline layer (all ``SKIPPED``) and for a harness the
driver cannot run (whole-harness skip), so the matrix still shows the
declared column and the skip reason per cell.
"""
cells = [
_cell(
probe,
profile,
observed if _applicable(probe, profile) else ProbeResult.not_applicable(),
)
for probe in probes
]
return HarnessReport(
profile=profile, cells=cells, skipped_reason=skipped_reason, transport=transport
)
def _as_sink(progress: Progress | ProgressSink | None) -> ProgressSink | None:
"""Normalize the ``progress`` argument to a :class:`ProgressSink`.
Accepts a structured sink (used as-is), a plain line callback (adapted to a
:class:`~tests.harness_bench.events.LineSink`), or ``None`` (silent).
"""
if progress is None:
return None
if isinstance(progress, ProgressSink):
return progress
return LineSink(progress) # a bare callable → line output
def _emit(sink: ProgressSink | None, event) -> None:
if sink is not None:
sink.emit(event)
async def run_harness(
profile: BenchProfile,
*,
probes: list[CapabilityProbe] | None = None,
databricks_profile: str | None = None,
live: bool = True,
transport: str | None = None,
fast: bool = False,
progress: Progress | ProgressSink | None = None,
shared_full_server=None,
) -> HarnessReport:
"""Run every applicable probe against one harness.
:param profile: The harness under test.
:param probes: Probes to run; defaults to :data:`ALL_PROBES`.
:param databricks_profile: Gateway profile for live turns. Required
for ``live=True``; its absence skips the whole harness.
:param live: When ``False``, produce a declared-only report (every
cell ``SKIPPED`` with an "offline" note) without spawning
anything — used for a fast ``--list``/dry render.
:param transport: ``--transport`` override; wins over the profile's
family default (see :func:`resolve_driver_class`).
:param fast: ``--fast`` — downgrade the SDK family to sdk-inproc (skip the
server boot, trading Tool calling + Policy DENY coverage).
:param progress: A :class:`ProgressSink` (structured events), a plain
per-line callback (adapted), or ``None`` (silent).
:param shared_full_server: An optional shared
:class:`~tests.harness_bench.full_server_driver.SharedFullServer` to
register this harness on, instead of the driver spawning its own
server+runner. Only used when the resolved driver is the full-server
driver; ignored otherwise.
:returns: The :class:`HarnessReport`.
"""
probes = probes if probes is not None else ALL_PROBES
sink = _as_sink(progress)
if not live:
resolved = resolve_transport_name(profile, override=transport, fast=fast)
return _uniform_report(
profile, probes, ProbeResult.skipped("offline (declared shown)"), transport=resolved
)
driver_cls = resolve_driver_class(profile, override=transport, fast=fast)
resolved_transport = driver_cls.transport
unavailable = driver_cls.unavailable(profile, databricks_profile=databricks_profile)
if unavailable is not None:
_emit(sink, HarnessSkipped(profile.harness, unavailable, resolved_transport))
return _uniform_report(
profile,
probes,
ProbeResult.skipped(unavailable),
skipped_reason=unavailable,
transport=resolved_transport,
)
_emit(sink, HarnessStarted(profile.harness, driver_cls.transport, profile.model))
cells: list[CellResult] = []
if shared_full_server is not None and driver_cls.transport == "full-server":
driver_cm = driver_cls(
profile, databricks_profile=databricks_profile, shared=shared_full_server
)
else:
driver_cm = driver_cls(profile, databricks_profile=databricks_profile)
try:
entered = await driver_cm.__aenter__()
except Exception as exc:
# Provisioning failures skip one harness without aborting a matrix run.
if isinstance(exc, ProvisioningError):
_logger.info("skipping %s: %s", profile.harness, exc)
else:
_logger.warning("provisioning failed for %s", profile.harness, exc_info=True)
with contextlib.suppress(Exception):
await driver_cm.__aexit__(type(exc), exc, exc.__traceback__)
reason = f"provisioning failed: {exc}"
_emit(sink, HarnessSkipped(profile.harness, reason, resolved_transport))
return _uniform_report(
profile,
probes,
ProbeResult.skipped(reason),
skipped_reason=reason,
transport=resolved_transport,
)
try:
driver = entered
prereq_skip: str | None = None
for probe in probes:
if not _applicable(probe, profile):
cells.append(_cell(probe, profile, ProbeResult.not_applicable()))
continue
if prereq_skip is not None:
observed = ProbeResult.skipped(prereq_skip)
else:
_emit(sink, ProbeStarted(profile.harness, probe.name, probe.title))
try:
observed = await probe.run(driver, profile)
except Exception as exc:
observed = ProbeResult(Verdict.UNKNOWN, note=f"probe raised: {exc!r}")
_emit(
sink,
ProbeFinished(
profile.harness, probe.name, probe.title, observed.verdict, observed.note
),
)
cell = _cell(probe, profile, observed)
cells.append(cell)
if probe.name == _PREREQ_PROBE and cell.observed is not Verdict.SUPPORTED:
prereq_skip = f"prerequisite '{probe.title}' did not pass ({observed.note})"
finally:
await driver_cm.__aexit__(None, None, None)
_emit(sink, HarnessFinished(profile.harness))
return HarnessReport(profile=profile, cells=cells, transport=resolved_transport)
async def run_bench(
profiles: list[BenchProfile],
*,
probes: list[CapabilityProbe] | None = None,
databricks_profile: str | None = None,
live: bool = True,
transport: str | None = None,
fast: bool = False,
progress: Progress | ProgressSink | None = None,
jobs: int = 1,
) -> BenchMatrix:
"""Run the bench across *profiles* into a :class:`BenchMatrix`.
:param jobs: Max harnesses to run concurrently. ``1`` (default) is the
original sequential behavior. ``>1`` runs up to *jobs* harnesses at
once, bounded by a semaphore. Probes *within* a harness always run
sequentially — they share one driver/session with a single in-flight
turn — so concurrency is only across harnesses. Report order always
matches *profiles* order regardless of finish order.
For full-server harnesses under ``jobs`` > 1, one shared server+runner
is spawned and every full-server harness registers its own agent +
session on it (the runner resolves the harness per session), instead of
each harness booting its own server. native-tui harnesses still
self-provision (each needs its own host daemon).
"""
async with _maybe_shared_full_server(
profiles,
databricks_profile=databricks_profile,
live=live,
transport=transport,
fast=fast,
jobs=jobs,
) as shared:
if jobs <= 1:
reports = [
await run_harness(
p,
probes=probes,
databricks_profile=databricks_profile,
live=live,
transport=transport,
fast=fast,
progress=progress,
shared_full_server=shared,
)
for p in profiles
]
return BenchMatrix(reports=reports)
semaphore = asyncio.Semaphore(jobs)
async def _one(p: BenchProfile) -> HarnessReport:
async with semaphore:
return await run_harness(
p,
probes=probes,
databricks_profile=databricks_profile,
live=live,
transport=transport,
fast=fast,
progress=progress,
shared_full_server=shared,
)
reports = await asyncio.gather(*(_one(p) for p in profiles))
return BenchMatrix(reports=list(reports))
@contextlib.asynccontextmanager
async def _maybe_shared_full_server(
profiles: list[BenchProfile],
*,
databricks_profile: str | None,
live: bool,
transport: str | None,
fast: bool,
jobs: int,
):
"""Yield a shared full-server for parallel full-server runs, else ``None``.
Only stands one up when it actually helps: a live, parallel run with more
than one harness that resolves to the full-server transport. Otherwise
yields ``None`` and each harness provisions as before (a solo full-server
run still owns its own server, unchanged).
"""
shared = None
if live and jobs > 1:
full = [
p
for p in profiles
if resolve_driver_class(p, override=transport, fast=fast).transport == "full-server"
]
if len(full) > 1 and bench_creds_skip_reason(databricks_profile) is None:
shared = SharedFullServer(resolve_bench_env(databricks_profile))
await asyncio.to_thread(shared.__enter__)
try:
yield shared
finally:
if shared is not None:
await asyncio.to_thread(shared.__exit__, None, None, None)