La prima versione era poco leggibile: l'ombreggiatura usava solo l'inclinazione frontale, quindi le facce complanari di un pezzo piatto uscivano tutte dello stesso grigio e la forma non si distingueva. Due correzioni: - luce direzionale obliqua invece che frontale, con una rampa di luminosita' piu' ampia, cosi' le facce orientate diversamente si separano davvero; - soprattutto, gli spigoli. Sopra alle superfici si disegnano ora gli spigoli vivi (due facce che formano un angolo netto) e la silhouette (faccia visibile contro faccia nascosta). Sono le linee che rendono riconoscibile un pezzo meccanico, le stesse che traccerebbe un disegnatore. Per distinguere uno spigolo vivo da una suddivisione interna della tassellatura servono le facce adiacenti, che un STL non fornisce perche' ripete i vertici a ogni triangolo. Mesh.indexed() li fonde e ricostruisce la topologia: sul pezzo di prova 912 ripetizioni diventano 144 vertici reali, 304 facce e 456 spigoli, senza bordi liberi. La mesh risulta chiusa e la caratteristica di Eulero indica cinque fori passanti, come il pezzo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
270 lines
11 KiB
Python
270 lines
11 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 TestMesh(unittest.TestCase):
|
|
"""La forma arriva da una mesh esportata: il .prt non la espone."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
from creoparams.extract import find_mesh
|
|
cls.pairs = [(p, find_mesh(p)) for p in _parts()]
|
|
cls.pairs = [(p, m) for p, m in cls.pairs if m]
|
|
if not cls.pairs:
|
|
raise unittest.SkipTest("nessun .stl accanto ai modelli")
|
|
|
|
def test_lettura_stl(self):
|
|
from creoparams.mesh import read_stl
|
|
for part, stl in self.pairs:
|
|
with self.subTest(file=os.path.basename(stl)):
|
|
mesh = read_stl(stl)
|
|
self.assertGreater(len(mesh), 0)
|
|
self.assertTrue(all(len(t) == 3 for t in mesh.triangles))
|
|
self.assertTrue(all(len(v) == 3 for t in mesh.triangles for v in t))
|
|
|
|
def test_ingombro_positivo(self):
|
|
from creoparams.mesh import read_stl
|
|
for part, stl in self.pairs:
|
|
with self.subTest(file=os.path.basename(stl)):
|
|
mesh = read_stl(stl)
|
|
self.assertTrue(all(d > 0 for d in mesh.size))
|
|
self.assertGreater(mesh.volume(), 0)
|
|
self.assertGreater(mesh.area(), 0)
|
|
|
|
def test_geometria_nel_record(self):
|
|
for part, stl in self.pairs:
|
|
with self.subTest(file=os.path.basename(part)):
|
|
geometry = extract(part).get("geometry")
|
|
self.assertIsNotNone(geometry)
|
|
self.assertEqual(geometry["triangles"], len(read_stl_cached(stl)))
|
|
|
|
def test_pagina_html_autonoma(self):
|
|
"""La pagina deve funzionare offline: nessun riferimento esterno."""
|
|
from creoparams import viewer
|
|
from creoparams.mesh import read_stl
|
|
mesh = read_stl(self.pairs[0][1])
|
|
page = viewer.build(mesh, "prova", viewer.describe(mesh))
|
|
self.assertTrue(page.startswith("<!doctype html>"))
|
|
self.assertIn("</html>", page)
|
|
for marker in ('src="http', "href=\"http", "@import"):
|
|
self.assertNotIn(marker, page, "la pagina carica risorse esterne")
|
|
# la mesh deve essere incorporata, non referenziata
|
|
self.assertIn("const V = [[", page)
|
|
self.assertIn("], F = [[", page)
|
|
|
|
def test_topologia_spigoli(self):
|
|
"""Gli spigoli servono a rendere leggibile la forma: senza facce
|
|
adiacenti non si distinguono quelli vivi dalla tassellatura."""
|
|
from creoparams.mesh import read_stl
|
|
for part, stl in self.pairs:
|
|
with self.subTest(file=os.path.basename(stl)):
|
|
mesh = read_stl(stl)
|
|
vertices, faces, edges = mesh.indexed()
|
|
self.assertLess(len(vertices), len(mesh) * 3,
|
|
"i vertici ripetuti non sono stati fusi")
|
|
self.assertGreater(len(edges), len(faces))
|
|
# ogni spigolo appartiene ad almeno una faccia valida
|
|
for a, b, f0, f1 in edges:
|
|
self.assertNotEqual(a, b)
|
|
self.assertTrue(0 <= f0 < len(faces))
|
|
|
|
|
|
def read_stl_cached(path):
|
|
from creoparams.mesh import read_stl
|
|
return read_stl(path)
|
|
|
|
|
|
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()
|