124 lines
4.3 KiB
Python
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)
|