Files
pdm/test_regression.py
T
davideandClaude Opus 5 0410266334 Vista 3D del modello in una pagina HTML autonoma
Aggiunge --3d: genera un file HTML che si apre con un doppio clic e
permette di ruotare il pezzo col mouse, ingrandire con la rotella e
spostare col tasto destro. Mesh, stile e codice sono dentro la pagina:
nessuna libreria esterna e nessuna connessione, cosi' funziona anche da
una cartella di rete. Il disegno usa una canvas 2D con ordinamento per
profondita' invece di WebGL, che su alcune postazioni e' disattivato.

La forma non viene dal .prt ma da una mesh STL esportata da Creo, cercata
accanto al modello per nome (9258400207.prt.11 -> 9258400207.stl). Se
manca, il comando lo dice invece di fallire.

Perche' serve la mesh: la geometria nel .prt e' scritta nel formato del
kernel Granite di PTC ed e' compressa. Verificato con un attacco a testo
noto, presi da un export STEP i 22 valori esatti del modello (raggi 1,6 /
2,1 / 2,5 / 2,75 / 5,25 e coordinate fino a 75,0) e cercati nel file in
cinque codifiche su tutti i blocchi: nessun riscontro. La via ufficiale
per leggere i .prt nativi senza Creo e' il Granite Interoperability
Kernel di PTC, che si innesterebbe al posto di mesh.py senza toccare il
resto.

Quando la mesh c'e', il record guadagna un blocco "geometry" con
ingombro, volume e area. Il peso NON viene calcolato: servirebbe la
densita', che dipende dal materiale, e il materiale e' testo libero.
Sul 9258400207 il parametro dice "Anticordal 100", che e' alluminio:
applicare la densita' dell'acciaio darebbe 0,686 kg invece di 0,237.

Gli export (stl, dxf, dwg, iges, pdf) sono esclusi dal repository come i
modelli: sono geometria e tavole complete dei pezzi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:04:46 +02:00

253 lines
10 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 TRI = [[", page)
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()