9111eb2741
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
187 lines
7.7 KiB
Python
187 lines
7.7 KiB
Python
"""Test della specchiatura orizzontale/verticale per sorgente (#110): scripts/camera_service.py.
|
|
|
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
|
Copre: GET/PUT /camera/orientamento, effetto su /camera/grab e /camera/anteprima,
|
|
persistenza al riavvio, file rotto, validazione, scarto dei frame tenuti, avviso
|
|
sulla calibrazione densa. Sorgente: camera:folder su un'immagine 4x3 asimmetrica.
|
|
"""
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from test_calibrazione import ( # calibrazione densa reale (#84), per l'avviso
|
|
SORGENTE as SORGENTE_DENSA, cartella_densa, carica_su, client_denso, salva_con_nome,
|
|
)
|
|
|
|
__all__ = ["cartella_densa", "client_denso"] # fixture importate, restano usate
|
|
|
|
MODULO = Path(__file__).resolve().parents[1] / "camera_service.py"
|
|
SORGENTE = "camera:folder"
|
|
|
|
|
|
def _carica():
|
|
spec = importlib.util.spec_from_file_location("camera_service_orient", MODULO)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
@pytest.fixture
|
|
def cartella(tmp_path):
|
|
# #110: 4x3 tutta nera con UN pixel acceso in alto a sinistra (riga 0, colonna 0)
|
|
img = tmp_path / "foto"
|
|
img.mkdir()
|
|
f = np.zeros((3, 4, 3), np.uint8)
|
|
f[0, 0] = 255
|
|
cv2.imwrite(str(img / "a.png"), f)
|
|
return img
|
|
|
|
|
|
@pytest.fixture
|
|
def config(tmp_path):
|
|
return tmp_path / "cfg"
|
|
|
|
|
|
@pytest.fixture
|
|
def client(cartella, config):
|
|
mod = _carica()
|
|
with TestClient(mod.create_app(cartella, config)) as c:
|
|
yield c
|
|
|
|
|
|
def _acceso(png: bytes):
|
|
"""(riga, colonna) del pixel acceso nel PNG; controlla che sia uno solo."""
|
|
f = cv2.imdecode(np.frombuffer(png, np.uint8), cv2.IMREAD_COLOR)
|
|
assert f.shape[:2] == (3, 4)
|
|
r, c = np.nonzero(f.max(axis=2) > 128)
|
|
assert len(r) == 1
|
|
return int(r[0]), int(c[0])
|
|
|
|
|
|
def _imposta(c, orizz, vert, source=SORGENTE):
|
|
return c.put("/camera/orientamento", json={"source": source, "orizzontale": orizz, "verticale": vert})
|
|
|
|
|
|
def _grab(c):
|
|
r = c.post("/camera/grab", json={"source": SORGENTE})
|
|
assert r.status_code == 200, r.text
|
|
return _acceso(r.content)
|
|
|
|
|
|
def test_get_senza_file_false_false(client):
|
|
r = client.get("/camera/orientamento", params={"source": SORGENTE})
|
|
assert r.status_code == 200
|
|
assert r.json() == {"source": SORGENTE, "orizzontale": False, "verticale": False}
|
|
|
|
|
|
def test_get_senza_source_400(client):
|
|
assert client.get("/camera/orientamento").status_code == 400
|
|
|
|
|
|
@pytest.mark.parametrize("orizz,vert,atteso", [
|
|
(False, False, (0, 0)), # originale: alto a sinistra
|
|
(True, False, (0, 3)), # orizzontale: alto a destra
|
|
(False, True, (2, 0)), # verticale: basso a sinistra
|
|
(True, True, (2, 3)), # entrambi: basso a destra (180 gradi)
|
|
])
|
|
def test_grab_specchiato(client, orizz, vert, atteso):
|
|
r = _imposta(client, orizz, vert)
|
|
assert r.status_code == 200, r.text
|
|
assert (r.json()["orizzontale"], r.json()["verticale"]) == (orizz, vert)
|
|
assert client.get("/camera/orientamento", params={"source": SORGENTE}).json()["orizzontale"] is orizz
|
|
assert _grab(client) == atteso
|
|
|
|
|
|
def test_tutti_false_toglie_la_sorgente_dal_file(client, config):
|
|
_imposta(client, True, False)
|
|
assert SORGENTE in (config / "orientamento.json").read_text(encoding="utf-8")
|
|
_imposta(client, False, False)
|
|
assert SORGENTE not in (config / "orientamento.json").read_text(encoding="utf-8")
|
|
assert _grab(client) == (0, 0)
|
|
|
|
|
|
def test_anteprima_specchiata_dimensioni_invariate(client):
|
|
_imposta(client, True, True)
|
|
r = client.post("/camera/anteprima", json={"source": SORGENTE, "lato_max": 4096})
|
|
assert r.status_code == 200, r.text
|
|
assert (r.headers["x-larghezza"], r.headers["x-altezza"]) == ("4", "3")
|
|
f = cv2.imdecode(np.frombuffer(r.content, np.uint8), cv2.IMREAD_GRAYSCALE)
|
|
assert f.shape == (3, 4)
|
|
# JPEG con perdite: il pixel acceso e' il piu' chiaro, in basso a destra
|
|
assert np.unravel_index(int(np.argmax(f)), f.shape) == (2, 3)
|
|
|
|
|
|
def test_persistenza_al_riavvio(cartella, config):
|
|
mod = _carica()
|
|
with TestClient(mod.create_app(cartella, config)) as c1:
|
|
assert _imposta(c1, True, False).status_code == 200
|
|
with TestClient(_carica().create_app(cartella, config)) as c2:
|
|
j = c2.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
|
assert (j["orizzontale"], j["verticale"]) == (True, False)
|
|
assert _grab(c2) == (0, 3)
|
|
|
|
|
|
def test_file_rotto_non_blocca_l_avvio(cartella, config):
|
|
config.mkdir(parents=True)
|
|
(config / "orientamento.json").write_text("{non e json", encoding="utf-8")
|
|
with TestClient(_carica().create_app(cartella, config)) as c:
|
|
j = c.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
|
assert (j["orizzontale"], j["verticale"]) == (False, False)
|
|
assert _imposta(c, False, True).status_code == 200 # e si puo' riscrivere
|
|
|
|
|
|
def test_put_campi_non_bool_400(client):
|
|
for corpo in ({"source": SORGENTE, "orizzontale": 1, "verticale": False},
|
|
{"source": SORGENTE, "orizzontale": True, "verticale": "si"},
|
|
{"source": SORGENTE, "orizzontale": True},
|
|
{"orizzontale": True, "verticale": True}):
|
|
assert client.put("/camera/orientamento", json=corpo).status_code == 400, corpo
|
|
|
|
|
|
def test_put_scarta_i_frame_tenuti(client):
|
|
assert client.post("/camera/anteprima", json={"source": SORGENTE}).status_code == 200
|
|
assert client.post("/camera/blocca", json={"source": SORGENTE}).status_code == 200
|
|
_imposta(client, True, False)
|
|
assert client.post("/camera/blocca", json={"source": SORGENTE}).status_code == 404
|
|
# col frame tenuto scartato anche l'ultimo grezzo dell'anteprima e' sparito
|
|
assert client.post("/camera/anteprima/calibrata", json={"source": SORGENTE}).status_code == 404
|
|
|
|
|
|
def test_avviso_senza_mappa_e_null(client):
|
|
j = _imposta(client, True, False).json()
|
|
assert j["avviso"] is None
|
|
|
|
|
|
def test_avviso_con_mappa_attiva(client_denso):
|
|
densa = client_denso.post("/camera/calibrate", data={"source": SORGENTE_DENSA, "square_mm": "2"}).json()
|
|
assert salva_con_nome(client_denso, densa, "Densa").json()["metodo"] == "densa"
|
|
assert carica_su(client_denso, SORGENTE_DENSA, "Densa").status_code == 200
|
|
r = _imposta(client_denso, True, False, source=SORGENTE_DENSA)
|
|
assert r.status_code == 200, r.text
|
|
assert "«Densa»" in r.json()["avviso"] and "va rifatta" in r.json()["avviso"]
|
|
# stesso valore di nuovo: nulla e' cambiato, nessun avviso
|
|
assert _imposta(client_denso, True, False, source=SORGENTE_DENSA).json()["avviso"] is None
|
|
|
|
|
|
def test_scrittura_fallita_500_e_orientamento_di_prima(cartella, config, monkeypatch):
|
|
# #110: se orientamento.json non si scrive, in memoria resta l'orientamento di prima
|
|
mod = _carica()
|
|
with TestClient(mod.create_app(cartella, config)) as c:
|
|
assert _imposta(c, True, False).status_code == 200 # stato di partenza: orizzontale
|
|
def rotto(*a, **k):
|
|
raise OSError("disco pieno")
|
|
monkeypatch.setattr(mod.os, "replace", rotto)
|
|
assert _imposta(c, False, True).status_code == 500
|
|
j = c.get("/camera/orientamento", params={"source": SORGENTE}).json()
|
|
assert (j["orizzontale"], j["verticale"]) == (True, False)
|
|
assert _grab(c) == (0, 3) # ancora solo orizzontale
|
|
# sorgente che prima non c'era: la voce non resta
|
|
assert c.put("/camera/orientamento", json={"source": "altra", "orizzontale": True, "verticale": True}).status_code == 500
|
|
j = c.get("/camera/orientamento", params={"source": "altra"}).json()
|
|
assert (j["orizzontale"], j["verticale"]) == (False, False)
|