"""Test dell'archivio unico di calibrazioni con nome (#91): scripts/calibrazione.py, classe Archivio. Uso: uv run --python 3.13 --extra vision-worker --extra dev \ --with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q Solo tmp_path: niente ProgramData, niente camera. """ import json import pytest from test_calibrazione import _carica, una_scacchiera_densa @pytest.fixture(scope="module") def mappa_densa(): # #91: una mappa vera, calcolata una volta sola per tutti i test del modulo _carica() import calibrazione return calibrazione.calibra_scacchiera_densa(una_scacchiera_densa(), 2.0)[1] def _dati(**extra): base = {"px_per_mm": 20.0, "mm_per_px": 0.05, "image_width": 640, "image_height": 480, "metodo": "planare", "square_mm": 2.0, "calibrated_at": "2026-10-24T10:00:00+00:00", "origine": "file:s.png"} return {**base, **extra} @pytest.fixture def cal(): _carica() import calibrazione return calibrazione # ---- nomi @pytest.mark.parametrize("nome", ["", " ", "x" * 61, "a\x00b", "a\nb", "a\tb", None, 5]) def test_nome_non_valido(cal, tmp_path, nome): with pytest.raises(cal.NomeNonValido): cal.Archivio(tmp_path).salva_nome(nome, _dati()) def test_nome_valido_limiti_e_strip(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("x" * 60, _dati()) meta = arch.salva_nome(" Banco 1 ", _dati()) assert meta["nome"] == "Banco 1" # strip assert arch.leggi_nome("Banco 1")["nome"] == "Banco 1" def test_nome_unico_senza_maiuscole(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("Banco", _dati(px_per_mm=1.0)) with pytest.raises(cal.NomeGiaUsato, match="Banco"): arch.salva_nome("BANCO", _dati(px_per_mm=2.0)) assert arch.leggi_nome("banco")["px_per_mm"] == 1.0 # non toccata assert len(arch.elenco()) == 1 def test_sovrascrivi_sostituisce_e_prende_il_nuovo_nome(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("Banco", _dati(px_per_mm=1.0)) arch.salva_nome("BANCO", _dati(px_per_mm=2.0), sovrascrivi=True) voci = arch.elenco() assert len(voci) == 1 and voci[0]["nome"] == "BANCO" and voci[0]["px_per_mm"] == 2.0 assert len(list((tmp_path / "calibrazioni").glob("*.json"))) == 1 def test_salva_non_scrive_source_ne_overlay(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("A", _dati(source="camera:x", overlay_png="abc", saved=True)) scritto = json.loads(next((tmp_path / "calibrazioni").glob("*.json")).read_text(encoding="utf-8")) assert "source" not in scritto and "overlay_png" not in scritto and "saved" not in scritto assert scritto["nome"] == "A" and scritto["origine"] == "file:s.png" def test_leggi_nome_inesistente_o_non_valido(cal, tmp_path): arch = cal.Archivio(tmp_path) assert arch.leggi_nome("nessuna") is None and arch.leggi_nome("") is None # ---- elenco e attive def test_elenco_dalla_piu_recente_e_attiva_su(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("vecchia", _dati(calibrated_at="2026-01-01T00:00:00+00:00")) arch.salva_nome("nuova", _dati(calibrated_at="2026-10-01T00:00:00+00:00")) arch.attiva("camera:b", "nuova") arch.attiva("camera:a", "nuova") voci = arch.elenco() assert [v["nome"] for v in voci] == ["nuova", "vecchia"] assert voci[0]["attiva_su"] == ["camera:a", "camera:b"] and voci[1]["attiva_su"] == [] assert set(voci[0]) == {"nome", "origine", "image_width", "image_height", "px_per_mm", "mm_per_px", "metodo", "square_mm", "calibrated_at", "attiva_su"} def test_attiva_leggi_scollega(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("Banco", _dati()) assert arch.attiva_di("camera:a") is None and arch.leggi("camera:a") is None assert arch.attiva("camera:a", "banco") == "Banco" # nome come in libreria assert arch.attiva_di("camera:a") == "Banco" letta = arch.leggi("camera:a") assert letta["nome"] == "Banco" and letta["source"] == "camera:a" and letta["px_per_mm"] == 20.0 assert json.loads((tmp_path / "attive.json").read_text(encoding="utf-8")) == {"camera:a": "Banco"} arch.scollega("camera:a") assert arch.attiva_di("camera:a") is None and arch.leggi("camera:a") is None assert arch.leggi_nome("Banco") is not None # la libreria resta arch.scollega("camera:a") # idempotente def test_attiva_nome_sconosciuto(cal, tmp_path): with pytest.raises(cal.NomeSconosciuto): cal.Archivio(tmp_path).attiva("camera:a", "nessuna") def test_due_sorgenti_stessa_calibrazione(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("Comune", _dati()) arch.attiva("camera:a", "Comune") arch.attiva("camera:b", "Comune") assert arch.leggi("camera:a")["source"] == "camera:a" and arch.leggi("camera:b")["source"] == "camera:b" def test_elimina_nome_toglie_da_attive_e_mappa(cal, tmp_path, mappa_densa): arch = cal.Archivio(tmp_path) arch.salva_nome("Densa", _dati(metodo="densa"), mappa_densa) arch.salva_nome("Altra", _dati()) arch.attiva("camera:a", "Densa") arch.attiva("camera:b", "Densa") arch.attiva("camera:c", "Altra") assert arch.leggi_mappa("camera:a") is not None assert arch.elimina_nome("DENSA") is True assert arch.leggi_nome("Densa") is None and not list((tmp_path / "calibrazioni").glob("*.npz")) assert arch.attiva_di("camera:a") is None and arch.attiva_di("camera:b") is None assert arch.leggi_mappa("camera:a") is None assert arch.attiva_di("camera:c") == "Altra" assert json.loads((tmp_path / "attive.json").read_text(encoding="utf-8")) == {"camera:c": "Altra"} assert arch.elimina_nome("Densa") is False # gia' tolta # ---- mappa def test_leggi_mappa_risolve_l_attiva_con_cache(cal, tmp_path, mappa_densa): arch = cal.Archivio(tmp_path) assert arch.leggi_mappa("camera:a") is None arch.salva_nome("Densa", _dati(metodo="densa"), mappa_densa) assert arch.leggi_mappa("camera:a") is None # non attiva su camera:a arch.attiva("camera:a", "Densa") prima = arch.leggi_mappa("camera:a") assert prima is not None and arch.leggi_mappa("camera:a") is prima # cache in memoria assert arch.leggi_mappa("camera:b") is None assert not list(tmp_path.rglob("*.tmp*")) # niente temporanei rimasti arch.salva_nome("Densa", _dati(metodo="densa"), mappa_densa, sovrascrivi=True) # file riscritto assert arch.leggi_mappa("camera:a") is not prima # cache invalidata arch.salva_nome("Densa", _dati(), sovrascrivi=True) # sovrascritta con una planare: la mappa sparisce assert arch.leggi_mappa("camera:a") is None and not list(tmp_path.rglob("*.npz")) # ---- migrazione dal formato vecchio def _vecchia(cartella, cal, source, mappa=None, **extra): """Scrive una calibrazione nel formato vecchio (un JSON per sorgente, + .npz) come faceva Archivio.salva.""" dati = {"source": source, **_dati(**extra)} dati.pop("origine") nudo = cal.Archivio.__new__(cal.Archivio) # senza costruttore: niente migrazione mentre si prepara nudo.cartella = cartella percorso = nudo._file(source) cartella.mkdir(parents=True, exist_ok=True) percorso.write_text(json.dumps(dati), encoding="utf-8") if mappa is not None: mappa.save(str(percorso.with_suffix(".npz"))) return percorso def test_migrazione_formato_vecchio(cal, tmp_path, mappa_densa): v1 = _vecchia(tmp_path, cal, "camera:601020", mappa_densa, metodo="densa", px_per_mm=7.0) v2 = _vecchia(tmp_path, cal, "camera:folder", px_per_mm=9.0) (tmp_path / "settaggi").mkdir() (tmp_path / "settaggi" / v1.name).write_text("{}", encoding="utf-8") # non si tocca arch = cal.Archivio(tmp_path) assert not v1.exists() and not v1.with_suffix(".npz").exists() and not v2.exists() assert {v["nome"] for v in arch.elenco()} == {"camera:601020", "camera:folder"} assert arch.attiva_di("camera:601020") == "camera:601020" and arch.attiva_di("camera:folder") == "camera:folder" letta = arch.leggi("camera:601020") assert letta["px_per_mm"] == 7.0 and letta["source"] == "camera:601020" assert arch.leggi_mappa("camera:601020") is not None # il .npz ha seguito il JSON assert len(list((tmp_path / "calibrazioni").glob("*.npz"))) == 1 assert (tmp_path / "settaggi" / v1.name).read_text(encoding="utf-8") == "{}" def test_migrazione_idempotente(cal, tmp_path): _vecchia(tmp_path, cal, "camera:a") cal.Archivio(tmp_path) prima = sorted(p.name for p in tmp_path.rglob("*") if p.is_file()) arch = cal.Archivio(tmp_path) # secondo avvio arch.migra() assert sorted(p.name for p in tmp_path.rglob("*") if p.is_file()) == prima assert [v["nome"] for v in arch.elenco()] == ["camera:a"] def test_migrazione_nome_gia_preso(cal, tmp_path): arch = cal.Archivio(tmp_path) arch.salva_nome("camera:a", _dati(px_per_mm=1.0, calibrated_at="2020-01-01T00:00:00+00:00")) _vecchia(tmp_path, cal, "camera:a", px_per_mm=5.0) arch.migra() nomi = {v["nome"]: v["px_per_mm"] for v in arch.elenco()} assert nomi == {"camera:a": 1.0, "camera:a (2)": 5.0} assert arch.attiva_di("camera:a") == "camera:a (2)" # la sorgente usa la sua, non quella gia' presente # un altro file con lo stesso nome -> (3) _vecchia(tmp_path, cal, "camera:A", px_per_mm=6.0) # nome uguale senza maiuscole arch.migra() assert "camera:A (3)" in {v["nome"] for v in arch.elenco()} def test_migrazione_ignora_json_senza_source_e_attive(cal, tmp_path): tmp_path.mkdir(exist_ok=True) (tmp_path / "altro.json").write_text(json.dumps({"x": 1}), encoding="utf-8") (tmp_path / "rotto.json").write_text("{non json", encoding="utf-8") arch = cal.Archivio(tmp_path) arch.salva_nome("A", _dati()) arch.attiva("source", "A") # una sorgente chiamata «source» non deve far scambiare attive.json per un vecchio file arch.migra() assert (tmp_path / "altro.json").exists() and (tmp_path / "rotto.json").exists() assert arch.attiva_di("source") == "A" and [v["nome"] for v in arch.elenco()] == ["A"] def test_cartella_assente_non_da_errore(cal, tmp_path): arch = cal.Archivio(tmp_path / "non" / "esiste") assert arch.elenco() == [] and arch.leggi("s") is None and arch.leggi_mappa("s") is None arch.scollega("s") assert arch.elimina_nome("x") is False