0a3d1f5222
testpaths include src/vision/tests: prima uv run pytest (365 test) non ci passava mai. Niente conftest.py: un pytest.importorskip a livello di modulo in conftest.py rompe l'intera sessione, perche' Skipped eredita da BaseException e _importconftest cattura solo Exception (verificato con traceback, provato sia su src/vision/tests sia su src/vision come radice). La guardia sta invece in cima a test_runner.py, prima di numpy (assente anch'esso su 3.11) - idioma standard di pytest, cattura pulita a livello di collection. README aggiornato con il comando reale per farli girare.
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""Il runner: grafo più immagine, numeri fuori. Niente HTTP, niente database."""
|
|
import pytest
|
|
|
|
# The vision tree requires Python >=3.13 (vs-task/vs-measure/vs-pm2d declare it
|
|
# upstream, see pyproject.toml's `vision` extra), while backend and frontend
|
|
# stay on 3.11. This must run before `import numpy` - numpy itself is only
|
|
# installed alongside the vision extra - so that `uv run pytest` on the
|
|
# default 3.11 environment reports this module as skipped, with a reason,
|
|
# instead of erroring out of the whole collection.
|
|
pytest.importorskip(
|
|
"visionsuite.task",
|
|
reason=(
|
|
"the vision tree requires Python >=3.13 (vs-task/vs-measure/vs-pm2d "
|
|
"declare it upstream); run it with "
|
|
"`uv run --python 3.13 --extra vision --extra dev pytest src/vision/tests`"
|
|
),
|
|
)
|
|
|
|
import numpy as np
|
|
|
|
from src.vision.runner import RunOutcome, engine_version, run_graph
|
|
|
|
|
|
def a_disc(radius: int = 60, size: int = 400) -> np.ndarray:
|
|
"""Un disco chiaro su fondo scuro: un pezzo finto ma misurabile."""
|
|
image = np.zeros((size, size), dtype=np.uint8)
|
|
yy, xx = np.ogrid[:size, :size]
|
|
centre = size // 2
|
|
image[(yy - centre) ** 2 + (xx - centre) ** 2 <= radius ** 2] = 255
|
|
return image
|
|
|
|
|
|
def a_circle_graph() -> dict:
|
|
"""Un grafo di due strumenti, costruito con l'API di vs-task."""
|
|
from visionsuite.task import Task
|
|
from visionsuite.task.tools import DetectCircleTool, DiameterTool
|
|
|
|
task = Task(name="prova")
|
|
found = task.add(DetectCircleTool(name="disco"))
|
|
found.center_row, found.center_col, found.radius = 200.0, 200.0, 60.0
|
|
found.scan_size = 30.0
|
|
|
|
quota = task.add(DiameterTool(name="diametro"))
|
|
quota.link("circle", found, "circle")
|
|
return task.to_dict()
|
|
|
|
|
|
def test_the_graph_produces_named_outputs():
|
|
outcome = run_graph(a_disc(), a_circle_graph())
|
|
|
|
assert isinstance(outcome, RunOutcome)
|
|
assert outcome.failures == []
|
|
# Le chiavi sono «id dello strumento punto nome dell'uscita»: è la stessa
|
|
# forma che la subtask salva in vision_output.
|
|
assert any(key.endswith(".diameter") for key in outcome.outputs)
|
|
diameter = next(v for k, v in outcome.outputs.items() if k.endswith(".diameter"))
|
|
assert diameter == pytest.approx(120.0, abs=4.0)
|
|
|
|
|
|
def test_a_broken_tool_does_not_sink_the_others():
|
|
"""vs-task non ferma l'esecuzione su un errore: il runner lo riporta."""
|
|
graph = a_circle_graph()
|
|
graph["tools"].append(
|
|
{"id": "ghost", "kind": "DetectCircle", "name": "assente",
|
|
"properties": {"center_row": 5.0, "center_col": 5.0, "radius": 900.0}}
|
|
)
|
|
|
|
outcome = run_graph(a_disc(), graph)
|
|
|
|
assert [f.tool_id for f in outcome.failures] == ["ghost"]
|
|
assert any(key.endswith(".diameter") for key in outcome.outputs)
|
|
|
|
|
|
def test_the_engine_says_which_version_it_is():
|
|
version = engine_version()
|
|
assert len(version) == 40 # un SHA di git, per intero
|