Files
DumiFlex-ComfyUI-Wildcard-P…/wp_api/test_runner.py
T

124 lines
4.3 KiB
Python

"""/wp/api/test — runs N samples of the resolver against an ad-hoc snapshot."""
from __future__ import annotations
import random
from collections import Counter
from aiohttp import web
from engine.db.repositories import ModuleRepository
from engine.modules.dispatcher import UnknownModuleType, resolve_module
from engine.modules.snapshot import walk_transitive_refs
from wp_api._helpers import db_session, extract_referenced_uuids, json_error, json_ok
_MAX_SAMPLES = 10000
def _build_request_catalog(request: web.Request, body: dict) -> dict[str, dict]:
"""Lazy-walk the request body for `@{8hex}` refs and load just the
referenced wildcards (plus transitive deps) from the DB.
Spec §2.10 — never the full library. Run ONCE per request; the catalog
is stable across all sample iterations within the same `run_test` call,
so callers should hoist this above their per-sample loop and reuse the
returned dict for every `__wp_catalog__` injection."""
roots = extract_referenced_uuids(body.get("payload"))
roots |= extract_referenced_uuids(body.get("instance"))
if not roots:
return {}
with db_session(request) as conn:
repo = ModuleRepository(conn)
def _fetch(uuid: str) -> dict | None:
try:
return repo.get_by_uuid(uuid)
except Exception:
return None
return walk_transitive_refs(roots, fetch_module=_fetch).snapshots
def _make_sample_ctx(catalog: dict[str, dict]) -> dict:
"""Per-sample ctx — fresh `__wp_rng__` and `__wp_warnings__`, but the
`__wp_catalog__` reference is shared across all samples (the resolver
treats it as read-only)."""
return {
"__wp_rng__": random.Random(),
"__wp_warnings__": [],
"__wp_catalog__": catalog,
}
async def run_test(request: web.Request) -> web.Response:
try:
body = await request.json()
except Exception:
return json_error("invalid JSON body", status=400)
if not isinstance(body, dict):
return json_error("body must be a JSON object", status=400)
required = {"type", "payload", "instance", "samples"}
missing = required - body.keys()
if missing:
return json_error(f"missing fields: {sorted(missing)}", status=400)
try:
samples = int(body["samples"])
except (TypeError, ValueError):
return json_error("samples must be an integer", status=400)
if samples < 1 or samples > _MAX_SAMPLES:
return json_error(f"samples must be 1..{_MAX_SAMPLES}", status=400)
snap = {
"type": body["type"],
"payload": body["payload"],
"instance": body["instance"],
}
# Build the runtime catalog ONCE per request (§2.10 lazy walk). The
# walked dict is stable across all sample iterations — only the rng
# and warnings list need to be fresh per sample.
catalog = _build_request_catalog(request, body)
results: list[dict[str, str]] = []
all_warnings: list[dict] = []
try:
for _ in range(samples):
ctx = _make_sample_ctx(catalog)
results.append(resolve_module(snap, ctx=ctx))
all_warnings.extend(ctx["__wp_warnings__"])
except UnknownModuleType as e:
return json_error(f"unknown module type: {e.args[0]!r}", status=400)
histogram: Counter[str] = Counter()
for r in results:
for v in r.values():
histogram[v] += 1
# Deduplicate warnings by (type, message) to avoid N*samples noise.
seen: set[tuple] = set()
deduped_warnings: list[dict] = []
for w in all_warnings:
key = (w.get("type"), w.get("message"))
if key not in seen:
seen.add(key)
deduped_warnings.append(w)
# Extract flat sample values for the SPA Test Runner display. Each
# resolve_module call returns {binding: value}; we surface the values.
sample_values: list[str] = []
for r in results:
for v in r.values():
sample_values.append(v)
if not r:
sample_values.append("")
return json_ok({
"results": results,
"histogram": dict(histogram),
"samples": sample_values, # NEW: additive top-level
"warnings": deduped_warnings, # NEW: additive top-level
})
def register(router) -> None:
router.add_post("/wp/api/test", run_test)