ebaafbe6e9
Worker: POST /dxf/preview legge il DXF con read_xld_dxf (lo stesso lettore di CompareDxf) e ritorna contorni in mm con y verso il basso, bbox, dimensioni, entita e punti (tetto 20000, decimazione uniforme). Backend: POST /api/vision/dxf-preview (Maker) che inoltra al worker; 422 inoltrato. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
147 lines
5.3 KiB
Python
147 lines
5.3 KiB
Python
"""POST /recipes/dxf-compare del worker (#83): forma della risposta ed errori."""
|
|
import pytest
|
|
|
|
# Come test_worker.py: serve l'albero vision (Python >=3.13), altrimenti skipped.
|
|
pytest.importorskip(
|
|
"visionsuite.task",
|
|
reason="the vision tree requires Python >=3.13; see test_worker.py",
|
|
)
|
|
|
|
import base64
|
|
import io
|
|
import time
|
|
|
|
import numpy as np
|
|
from httpx import ASGITransport, AsyncClient
|
|
from PIL import Image
|
|
|
|
from src.vision.tests.test_costruttori import PX_PER_MM, SAMPLE_DXF, a_scene
|
|
from src.vision_worker.main import app
|
|
|
|
|
|
def _png(array: np.ndarray) -> bytes:
|
|
buffer = io.BytesIO()
|
|
Image.fromarray(array).save(buffer, format="PNG")
|
|
return buffer.getvalue()
|
|
|
|
|
|
async def _post(image: bytes, dxf: bytes, **data):
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://worker") as client:
|
|
return await client.post(
|
|
"/recipes/dxf-compare",
|
|
files={"image": ("riferimento.png", image, "image/png"),
|
|
"dxf": ("disegno.dxf", dxf, "application/octet-stream")},
|
|
data={"px_per_mm": str(PX_PER_MM), "band_mm": "2.0", **data},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dxf_compare_200_con_le_chiavi_del_contratto():
|
|
inizio = time.monotonic()
|
|
response = await _post(_png(a_scene(0.0)), SAMPLE_DXF.read_bytes())
|
|
durata = time.monotonic() - inizio
|
|
|
|
assert response.status_code == 200, response.text
|
|
body = response.json()
|
|
assert {"graph", "files", "outputs", "overlay", "outcome"} <= set(body)
|
|
assert set(body["files"]) == {"disegno.dxf", "calibrazione.json"}
|
|
assert base64.b64decode(body["files"]["disegno.dxf"]) == SAMPLE_DXF.read_bytes()
|
|
assert set(body["outputs"]) == {
|
|
"dxf.points_in_pct", "dxf.distance_avg", "dxf.distance_min", "dxf.distance_max"}
|
|
assert body["outputs"]["dxf.points_in_pct"] > 95.0
|
|
assert body["graph"]["schema_version"] == 2
|
|
print(f"\nW2 sulla scena di prova: {durata:.2f}s") # riportato nel rapporto
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dxf_compare_immagine_senza_pezzo_e_422():
|
|
bianca = np.full((600, 800), 255, np.uint8)
|
|
response = await _post(_png(bianca), SAMPLE_DXF.read_bytes())
|
|
assert response.status_code == 422
|
|
assert "pezzo" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dxf_compare_dxf_illeggibile_e_422():
|
|
response = await _post(_png(a_scene(0.0)), b"non e' un dxf")
|
|
assert response.status_code == 422
|
|
assert "DXF" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dxf_compare_immagine_non_decodificabile_e_422():
|
|
response = await _post(b"non e' un'immagine", SAMPLE_DXF.read_bytes())
|
|
assert response.status_code == 422
|
|
assert "not decodable" in response.json()["detail"].lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dxf_compare_parametro_fuori_range_e_422():
|
|
response = await _post(_png(a_scene(0.0)), SAMPLE_DXF.read_bytes(), polarity="grigio")
|
|
assert response.status_code == 422
|
|
|
|
|
|
# --- #101: POST /dxf/preview ---------------------------------------------------
|
|
|
|
def _rect_dxf(width: float, height: float, y0: float = 3.0) -> bytes:
|
|
"""DXF minimo con ezdxf: un rettangolo chiuso, y in alto rispetto a 0."""
|
|
import ezdxf
|
|
doc = ezdxf.new()
|
|
doc.modelspace().add_lwpolyline(
|
|
[(1, y0), (1 + width, y0), (1 + width, y0 + height), (1, y0 + height)], close=True,
|
|
)
|
|
buffer = io.StringIO()
|
|
doc.write(buffer)
|
|
return buffer.getvalue().encode("utf-8")
|
|
|
|
|
|
def _post_preview(dxf: bytes):
|
|
# TestClient sincrono: non serve pytest-asyncio (assente nel venv del worker)
|
|
from fastapi.testclient import TestClient
|
|
return TestClient(app).post(
|
|
"/dxf/preview", files={"dxf": ("d.dxf", dxf, "application/octet-stream")},
|
|
)
|
|
|
|
|
|
def test_dxf_preview_rettangolo_10x5():
|
|
resp = _post_preview(_rect_dxf(10, 5))
|
|
assert resp.status_code == 200, resp.text
|
|
body = resp.json()
|
|
assert body["larghezza_mm"] == 10 and body["altezza_mm"] == 5
|
|
assert body["entita"] == 1 and body["punti"] == sum(len(c) for c in body["contorni"])
|
|
# y ribaltata: il rettangolo stava a y 3..8 nel DXF, ora a -8..-3 (asse verso il basso)
|
|
assert body["bbox"] == {"xmin": 1, "ymin": -8, "xmax": 11, "ymax": -3}
|
|
ring = body["contorni"][0]
|
|
assert ring[0] == ring[-1] # contorno chiuso: primo == ultimo
|
|
assert all(-8 <= y <= -3 for _, y in ring)
|
|
|
|
|
|
@pytest.mark.parametrize("content", [b"", b"questo non e' un dxf"])
|
|
def test_dxf_preview_illeggibile_e_422(content):
|
|
resp = _post_preview(content)
|
|
assert resp.status_code == 422
|
|
assert resp.json()["detail"].startswith("DXF")
|
|
|
|
|
|
def test_dxf_preview_senza_geometria_e_422():
|
|
import ezdxf
|
|
doc = ezdxf.new()
|
|
buffer = io.StringIO()
|
|
doc.write(buffer)
|
|
resp = _post_preview(buffer.getvalue().encode("utf-8"))
|
|
assert resp.status_code == 422
|
|
assert "senza geometria" in resp.json()["detail"]
|
|
|
|
|
|
def test_dxf_preview_decima_oltre_il_tetto(monkeypatch):
|
|
from src.vision_worker import main
|
|
monkeypatch.setattr(main, "_PREVIEW_MAX_POINTS", 50)
|
|
import ezdxf
|
|
doc = ezdxf.new()
|
|
doc.modelspace().add_circle((0, 0), 20)
|
|
buffer = io.StringIO()
|
|
doc.write(buffer)
|
|
full = main._dxf_preview(buffer.getvalue().encode("utf-8"))
|
|
assert full["punti"] <= 50 + 1 and full["larghezza_mm"] == pytest.approx(40, abs=0.01) # dimensioni dai punti completi
|