Aggiunge tre capacita' al lettore, tutte senza dipendenze esterne.
Anteprima incorporata (preview.py)
Ogni file contiene una miniatura JPEG dell'ultimo salvataggio: il
render del pezzo nei modelli, l'immagine della tavola nei disegni.
Non e' referenziata dall'indice, quindi va cercata per firma: ogni
candidato viene validato percorrendone i segmenti JPEG, cosi' i byte
che iniziano per FFD8FF per caso non passano. Sui file campione il
risultato e' esattamente una immagine per file.
Decompressione compress(1) (lzw.py)
Creo comprime alcuni blocchi nel vecchio formato .Z. Rispetto a un
LZW generico va riprodotto l'allineamento a gruppi di otto codici:
senza, la decompressione produce qualche centinaio di byte corretti
e poi diverge silenziosamente.
Contenuto dei disegni (drawing.py)
Un .drw non ha la tabella parametri del modello ma contiene la
rappresentazione vettoriale della tavola. Ne estraiamo i testi:
cartiglio, note, tolleranze, quote. Le tavole diventano cosi'
ricercabili per contenuto. Nel disegno i valori compaiono gia'
risolti come testo, compreso il peso, mentre nel .prt sono ancora
in codifica binaria.
I blocchi oltre l'indice sono ora interpretati da ugc.iter_blocks, che
decomprime quando serve: lo stesso blocco puo' essere memorizzato
compresso in un file e in chiaro in un altro, quindi la scelta va fatta
guardando i byte e non il nome. Cercare soltanto gli stream compressi
faceva perdere i testi su un terzo dei disegni.
Sulla codifica dei numeri: sono double IEEE big-endian con i byte bassi
omessi, preceduti da un byte-tag. Lo dimostrano i disegni, dove size_x e
size_y valgono 420,0 x 297,0 (un A3 esatto). La mappatura del byte-tag
resta pero' incerta, quindi PESO e DENSITA continuano a essere riportati
come encoded_not_decoded: un valore plausibile ma sbagliato sarebbe
peggio di un valore assente.
La geometria 3D resta fuori portata, e ora e' verificato: in 200 KB di
blocchi geometrici non compare una sola costante 1.0 in nessuna codifica
in virgola mobile. Quei blocchi sono compressi con un algoritmo che non
espone firme note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
198 lines
7.8 KiB
Python
198 lines
7.8 KiB
Python
"""Test di regressione sui file campione presenti nella cartella.
|
|
|
|
Il parser si appoggia alla struttura binaria del formato Creo: qualsiasi
|
|
modifica alle espressioni di ricerca va verificata contro file reali. Il test
|
|
si limita ai file effettivamente presenti, quindi resta eseguibile anche su
|
|
un archivio diverso.
|
|
|
|
python3 -m unittest test_regression -v
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import unittest
|
|
|
|
from creoparams.extract import (extract, latest_versions, read_preview,
|
|
split_filename)
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
#: Parametri che ci aspettiamo di leggere da ogni .prt aziendale.
|
|
REQUIRED = ["CODICE", "DENOMINAZIONE", "REVISIONE", "STAMPIGLIATURA",
|
|
"MATERIALE", "DISEGNATORE", "DATA"]
|
|
|
|
#: Valori verificati manualmente. Vanno estesi man mano che si confrontano
|
|
#: altri file con Creo: e' questa la rete di sicurezza vera.
|
|
KNOWN = {
|
|
"9258400201.prt": {
|
|
"CODICE": "9258400201",
|
|
"DENOMINAZIONE": "Carter lato destro",
|
|
"MATERIALE": "ACC.INOX LAMIERA SP.4",
|
|
"DISEGNATORE": "Grilli",
|
|
"DATA": "9/02/2026",
|
|
},
|
|
"9258400205.prt": {
|
|
"CODICE": "9258400205",
|
|
"DENOMINAZIONE": "Tirante carter lateriali",
|
|
"MATERIALE": "AISI 304 TONDO D.12",
|
|
},
|
|
}
|
|
|
|
|
|
def _parts():
|
|
paths = latest_versions(glob.glob(os.path.join(HERE, "*.prt*")))
|
|
return [p for p in paths if split_filename(os.path.basename(p))[1] == "prt"]
|
|
|
|
|
|
class TestEstrazione(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.parts = _parts()
|
|
if not cls.parts:
|
|
raise unittest.SkipTest("nessun file .prt nella cartella")
|
|
cls.records = {
|
|
os.path.basename(p): extract(p) for p in cls.parts
|
|
}
|
|
|
|
def test_header_riconosciuto(self):
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
self.assertEqual(record["model"]["kind"], "PART")
|
|
self.assertTrue(record["model"]["creo_version"])
|
|
self.assertGreater(record["extraction"]["sections"], 0)
|
|
|
|
def test_parametri_obbligatori_presenti(self):
|
|
for name, record in self.records.items():
|
|
for param in REQUIRED:
|
|
with self.subTest(file=name, param=param):
|
|
entry = record["parameters"].get(param)
|
|
self.assertIsNotNone(entry, f"{param} non trovato")
|
|
self.assertEqual(entry["status"], "found")
|
|
|
|
def test_confidenza_alta(self):
|
|
"""Ogni parametro obbligatorio deve essere confermato in due
|
|
rappresentazioni indipendenti del file."""
|
|
for name, record in self.records.items():
|
|
for param in REQUIRED:
|
|
with self.subTest(file=name, param=param):
|
|
self.assertGreaterEqual(
|
|
record["parameters"][param]["confidence"], 0.95
|
|
)
|
|
|
|
def test_valori_noti(self):
|
|
for stem, expected in KNOWN.items():
|
|
matching = [
|
|
r for r in self.records.values()
|
|
if r["model"]["file_name"] == stem
|
|
]
|
|
if not matching:
|
|
continue
|
|
record = matching[0]
|
|
for param, value in expected.items():
|
|
with self.subTest(file=stem, param=param):
|
|
self.assertEqual(record["parameters"][param]["value"], value)
|
|
|
|
def test_codice_coerente_col_nome_file(self):
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
self.assertEqual(
|
|
record["parameters"]["CODICE"]["value"],
|
|
record["model"]["name"],
|
|
)
|
|
|
|
def test_anteprima_presente_e_valida(self):
|
|
for path in self.parts:
|
|
name = os.path.basename(path)
|
|
with self.subTest(file=name):
|
|
shot = read_preview(path)
|
|
self.assertIsNotNone(shot, "anteprima non trovata")
|
|
self.assertTrue(shot.data.startswith(b"\xff\xd8\xff"))
|
|
self.assertTrue(shot.data.endswith(b"\xff\xd9"))
|
|
self.assertGreater(shot.width, 0)
|
|
self.assertGreater(shot.height, 0)
|
|
# Le dimensioni dichiarate devono corrispondere al record.
|
|
declared = self.records[name]["preview"]
|
|
self.assertTrue(declared["present"])
|
|
self.assertEqual(declared["width"], shot.width)
|
|
|
|
def test_una_sola_anteprima_per_file(self):
|
|
"""Se il rilevatore trovasse piu' candidati, starebbe scambiando dati
|
|
binari qualsiasi per immagini."""
|
|
from creoparams import preview
|
|
for path in self.parts:
|
|
with self.subTest(file=os.path.basename(path)):
|
|
with open(path, "rb") as handle:
|
|
self.assertEqual(len(preview.find_all(handle.read())), 1)
|
|
|
|
def test_parametri_feature_separati(self):
|
|
"""I parametri dei fori non devono finire tra quelli del modello."""
|
|
for name, record in self.records.items():
|
|
for param, entry in record["parameters"].items():
|
|
if param in ("SCREW_SIZE", "DRILL_DIAMETER", "CSINK_ANGLE"):
|
|
with self.subTest(file=name, param=param):
|
|
self.assertEqual(entry["owner"], "feature")
|
|
|
|
|
|
class TestDisegni(unittest.TestCase):
|
|
"""I .drw non hanno parametri di modello ma contengono la tavola."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
paths = latest_versions(glob.glob(os.path.join(HERE, "*.drw*")))
|
|
cls.drawings = [p for p in paths
|
|
if split_filename(os.path.basename(p))[1] == "drw"]
|
|
if not cls.drawings:
|
|
raise unittest.SkipTest("nessun file .drw nella cartella")
|
|
cls.records = {os.path.basename(p): extract(p) for p in cls.drawings}
|
|
|
|
def test_riconosciuti_come_disegni(self):
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
self.assertEqual(record["model"]["kind"], "DRAWING")
|
|
self.assertFalse(record["extraction"]["holds_model_parameters"])
|
|
|
|
def test_testi_estratti(self):
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
sheet = record["drawing"]
|
|
self.assertGreater(sheet["decompressed_bytes"], 10_000)
|
|
self.assertGreater(len(sheet["texts"]), 20)
|
|
|
|
def test_voci_del_cartiglio(self):
|
|
"""Il cartiglio aziendale deve comparire in ogni tavola."""
|
|
attese = {"CODICE", "DISEGNATO", "STAMPIGLIATURA", "SCALA"}
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
testi = set(record["drawing"]["texts"])
|
|
self.assertTrue(attese.issubset(testi),
|
|
f"mancano {attese - testi}")
|
|
|
|
def test_primitive_grafiche(self):
|
|
for name, record in self.records.items():
|
|
with self.subTest(file=name):
|
|
prims = record["drawing"]["primitives"]
|
|
self.assertIn("prim_text", prims)
|
|
self.assertIn("prim_line", prims)
|
|
|
|
|
|
class TestLzw(unittest.TestCase):
|
|
def test_round_trip_noto(self):
|
|
"""Verifica il decoder su uno stream prodotto da compress(1), se
|
|
disponibile nel sistema."""
|
|
import shutil
|
|
import subprocess
|
|
from creoparams import lzw
|
|
tool = shutil.which("compress") or shutil.which("gzip")
|
|
if not tool:
|
|
self.skipTest("nessun compressore disponibile")
|
|
if "gzip" in tool:
|
|
self.skipTest("gzip non produce il formato .Z")
|
|
payload = b"PGL " * 500 + bytes(range(256)) * 4
|
|
proc = subprocess.run([tool, "-c"], input=payload, capture_output=True)
|
|
out, _ = lzw.decompress(proc.stdout)
|
|
self.assertEqual(out, payload)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|