Ctrl K
ComunidadGuías…

cookbook/07_testing_and_conformance.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026 OpenNVR
# SPDX-License-Identifier: Apache-2.0
"""Proving it works — `ConformanceRunner` and the tests worth writing.

Demonstrates: `ConformanceRunner`, `ConformanceReport`, `CheckOutcome`,
`CheckResult`, FastAPI's `TestClient` against an adapter, and the
`opennvr-adapter validate` / `conform` commands.

Conformance is the contract, executable: it probes every mandatory
endpoint, validates the wire shapes against the SDK's own Pydantic
models, and reports PASS / WARN / FAIL / SKIP. A green run means KAI-C
will accept the adapter — which is the only assurance a model developer
can get without a deployment to try it in.
"""
import base64

from fastapi.testclient import TestClient

from opennvr_adapter_sdk import Adapter
from opennvr_adapter_sdk.conformance import CheckOutcome, ConformanceRunner

adapter = Adapter("demo-tested", tasks=["object_detection"],
                  framework="onnxruntime")


@adapter.load()
def load():
    return object()


@adapter.on_image()
def detect(call):
    if not call.image:
        raise ValueError("a frame is required")
    return [call.detection("person", 0.9, 0.1, 0.1, 0.2, 0.3)]


FRAME = b"\xff\xd8\xff\xe0 pretend jpeg"


# ── The conformance run, in-process ────────────────────────────────


def test_the_adapter_conforms_to_the_contract():
    """What `opennvr-adapter validate .` runs, as a test — so a change
    that breaks the contract fails in CI rather than at install time.

    The runner's client is duck-typed, so TestClient drives the real
    ASGI app with no port bound and no network."""
    base_url = "http://adapter.test"
    with TestClient(adapter.app, base_url=base_url) as client:
        report = ConformanceRunner(base_url, client=client).run_all()

    assert report.is_green, [
        (r.name, r.detail) for r in report.results
        if r.outcome == CheckOutcome.FAIL
    ]


# ── The tests worth writing about YOUR model ───────────────────────


def test_it_answers_in_the_contract_shape():
    with TestClient(adapter.app) as client:
        response = client.post("/infer", json={
            "frame_b64": base64.b64encode(FRAME).decode(),
            "task": "object_detection",
        })
    assert response.status_code == 200
    body = response.json()
    assert body["status"] == "ok"
    detection = body["result"]["detections"][0]
    # Normalized coordinates. The single most common thing to get
    # wrong, and it is invisible until an operator sees a box in the
    # wrong place.
    assert 0.0 <= detection["bbox"]["x"] <= 1.0


def test_a_body_the_model_cannot_use_is_a_400_not_a_500():
    """The distinction decides whether KAI-C retries — so a bad frame
    classified as a 500 becomes a retry storm."""
    with TestClient(adapter.app) as client:
        response = client.post("/infer", json={"frame_b64": ""})
    assert response.status_code == 400
    assert response.json()["error"]["transient"] is False


def test_capabilities_advertise_a_task_and_a_fingerprint():
    """An adapter with no advertised task gets no work; one with a null
    fingerprint is silently exempt from drift detection."""
    with TestClient(adapter.app) as client:
        caps = client.get("/capabilities").json()
    assert caps["tasks_advertised"] == ["object_detection"]
    assert caps["model"]["fingerprint"]


def test_health_is_honest_about_a_failed_load():
    """A green dot on a dead adapter routes real work into a hole."""
    broken = Adapter("demo-broken", tasks=["object_detection"])
    broken.on_image()(lambda call: [])
    broken.load()(lambda: (_ for _ in ()).throw(RuntimeError("no weights")))

    service = broken.service
    service.load()
    assert service.is_ready() is False
    assert "no weights" in service.hardware_evaluation().reasoning


# Against a RUNNING adapter instead:
#
#     opennvr-adapter conform http://localhost:9001 --token $TOKEN
#     opennvr-adapter conform http://localhost:9001 --json > report.json