master
AdityaAngajala Merge PR #261: Require and verify molecule fabrication sources 7227f5f 1d ago
"""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()