skill
molecule-publish
Public Made by Adomby adom
Publish molecules AND generic boards to the Adom wiki - one publisher, two shapes: OCCT conversion (molecule mode with stats gates, or plain for boards), component packaging, adom-wiki publish, APM render assets + component dependencies. Companion to adom-molecule-import.
master
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
"""Regression checks for the shared export gate; run with unittest."""
import importlib.util
import json
import tempfile
import unittest
import zipfile
from pathlib import Path
from types import SimpleNamespace
SCRIPT = Path(__file__).resolve().parents[1] / "scripts/fab_bundle.py"
spec = importlib.util.spec_from_file_location("fab_bundle", SCRIPT)
fab = importlib.util.module_from_spec(spec)
spec.loader.exec_module(fab)
class ExportTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(dir=Path(__file__).resolve().parent)
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
assert self.root.resolve().is_relative_to(Path(__file__).resolve().parent)
self.layers = self.root / "fab"
self.layers.mkdir()
for ext in ("gtl", "gbl", "gts", "gbs", "gto", "gbo", "gm1"):
(self.layers / f"board.{ext}").write_bytes(b"G04 Test*\nX0Y0D02*\nX10Y10D01*\nM02*\n")
(self.layers / "board-PTH.drl").write_bytes(b"M48\nMETRIC,TZ\nT01C0.3\n%\nT01\nX0Y0\nM30\n")
(self.root / "board.step").write_bytes(b"STEP source")
(self.root / "board.kicad_pcb").write_bytes(b"saved board")
self.args = SimpleNamespace(root=str(self.root), slug="test-board", fab=str(self.layers),
source=["step=board.step", "board_kicad_pcb=board.kicad_pcb"],
tool="KiCad", tool_version="10.0.3", exporter="test",
exporter_version="1", stackup=None)
def test_preserves_original_members_and_checks_roundtrip(self):
rec = fab.create(self.args)
zipped = fab.read_zip(self.root / "test-board-fab.zip")
self.assertEqual(zipped, {p.name: p.read_bytes() for p in self.layers.iterdir()})
self.assertEqual(fab.check(self.root, "test-board"), rec)
def test_board_change_invalidates_record(self):
fab.create(self.args)
(self.root / "board.kicad_pcb").write_bytes(b"new revision")
with self.assertRaisesRegex(ValueError, "source changed"):
fab.check(self.root, "test-board")
def test_missing_bottom_layer_refused(self):
(self.layers / "board.gbs").unlink()
with self.assertRaisesRegex(ValueError, "mask_bottom"):
fab.create(self.args)
def test_empty_altium_outline_refused(self):
(self.layers / "board.gm1").write_bytes(b"G04 Header*\nM02*\n")
with self.assertRaisesRegex(ValueError, "no drawn segments"):
fab.create(self.args)
def test_stale_zip_record_refused_even_when_zip_hash_is_updated(self):
rec = fab.create(self.args)
zpath = self.root / "test-board-fab.zip"
files = fab.read_zip(zpath)
files["board.gtl"] += b"G04 altered*\n"
with zipfile.ZipFile(zpath, "w") as z:
for n, b in files.items():
z.writestr(n, b)
saved = rec["files"][-1]
saved.update(fab.fingerprint(zpath.name, "fab_zip", zpath.read_bytes()))
(self.root / "export.json").write_text(json.dumps(rec))
with self.assertRaisesRegex(ValueError, "zip entry changed"):
fab.check(self.root, "test-board")
def test_zip_path_traversal_refused_without_extraction(self):
path = self.root / "input.zip"
with zipfile.ZipFile(path, "w") as z:
z.writestr("../escape.gtl", b"anything")
with self.assertRaisesRegex(ValueError, "relative file path"):
fab.read_zip(path)
def test_x2_beats_extension_and_identifies_npth(self):
self.assertEqual(fab.role_for("misnamed.gtl", b"%TF.FileFunction,Legend,Bot*%"), "silk_bottom")
self.assertEqual(fab.role_for("board.TXT", b"M48\n; #@! TF.FileFunction,NonPlated,1,2,NPTH\n"), "drill_unplated")
self.assertEqual(fab.role_for("MpinTest-Plated.TXT", b"M48\nMETRIC,TZ\n"), "drill_plated")
self.assertEqual(fab.role_for("inner.G2", b"G04 gerber*\n"), "copper_inner_2")
if __name__ == "__main__":
unittest.main()