← Commit history

0.9.350 notes and the model check tests

John Lauer ·4feafd618a ·1mo ago ·parent 9a40be6
2 files changed +61
RELEASE_NOTES.md+14
@@ -10,6 +10,20 @@ unnamed. These entries were reconstructed after the fact from the work they contain, which is exactly the situation the mandatory-notes gate now prevents. Treat them as accurate but secondhand. +## 0.9.350  (2026-09-09 13:49 UTC)++kicad_model_check and model diagnostics on the 3D show verbs (#88)++## 0.9.350 (2026-09-09): 3D model diagnostics (#88)++- New read-only `kicad_model_check {library, footprintName}` or `{boardPath}`: per (model ...) reference it+  reports where KiCad would look (project dir, then the 3D search paths; never next to the .kicad_mod),+  whether the file exists, whether it is a STEP/VRML with geometry, hidden flags and implausible scale,+  with coded warnings and hints. Footprints that need no body (test points, fiducials, mounting holes,+  board-only parts) are exempt. `renderVerified` is always false: the viewer frame stays the render proof.+- `kicad_show_3d_chip` and `kicad_show_3d_board {filePath}` attach a `models` summary and a `_modelHint`+  when a body cannot be there, so a pads-only 3D view arrives with its reason.+ ## 0.9.349  (2026-09-09 13:17 UTC)  kicad_status carries operation state for timeout recovery (#87)
tests/test_model_check.pyadded+47
@@ -0,0 +1,47 @@+import sys, tempfile, unittest+from pathlib import Path+from unittest.mock import patch+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))+from handlers import model_check, install_footprint++STEP = b"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n#1=MANIFOLD_SOLID_BREP('x',#2);\n#2=CLOSED_SHELL('',());\nENDSEC;\nEND-ISO-10303-21;\n"++def fp_text(model_clause):+    return '(footprint "X" (version 20240108) (generator "t") (layer "F.Cu") (attr smd) (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu"))' + model_clause + ')'++class ModelCheck(unittest.TestCase):+    def setUp(self):+        self.tmp = tempfile.TemporaryDirectory(); d = Path(self.tmp.name)+        self.pretty = d / "Adom.pretty"; self.pretty.mkdir()+        self.base = d / "kicad"; (self.base / "share/kicad/3dmodels/Adom.3dshapes").mkdir(parents=True)+        (self.base / "share/kicad/3dmodels/Adom.3dshapes/GOOD.step").write_bytes(STEP)+        (self.pretty / "BARE.step").write_bytes(STEP)+        (self.pretty / "EMPTY.step").write_bytes(b"ISO-10303-21;\nHEADER;ENDSEC;DATA;ENDSEC;END-ISO-10303-21;")+        self.info = {"version": "10.0", "base_dir": str(self.base), "user_dir": str(d)}+        self.p1 = patch.object(model_check, "_library_dir", return_value=str(self.pretty)); self.p1.start()+        self.p2 = patch.object(model_check, "_kicad_common", return_value={}); self.p2.start()+    def tearDown(self): self.p1.stop(); self.p2.stop(); self.tmp.cleanup()+    def write(self, name, clause): (self.pretty / f"{name}.kicad_mod").write_text(fp_text(clause))+    def test_good_stock_reference(self):+        self.write("A", '(model "${KICAD10_3DMODEL_DIR}/Adom.3dshapes/GOOD.step" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))')+        r = model_check.check_footprint(self.info, "Adom", "A"); self.assertTrue(r["ok"], r); self.assertEqual(r["models"][0]["format"], "step"); self.assertGreater(r["models"][0]["geometryHints"], 0)+    def test_bare_relative_next_to_footprint_is_not_found(self):+        self.write("B", '(model "BARE.step" (offset (xyz 0 1.39 0.18)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 -90)))')+        r = model_check.check_footprint(self.info, "Adom", "B"); self.assertFalse(r["ok"]); self.assertEqual(r["warnings"][0]["code"], "not_found_relative"); self.assertIn("3DMODEL_DIR", r["warnings"][0]["hint"])+    def test_absolute_empty_geometry_and_hidden_and_scale(self):+        p = str(self.pretty / "EMPTY.step").replace("\\", "/")+        self.write("C", f'(model "{p}" (hide yes) (offset (xyz 0 0 0)) (scale (xyz 0 1 1)) (rotate (xyz 0 0 0)))')+        r = model_check.check_footprint(self.info, "Adom", "C"); codes = sorted(w["code"] for w in r["warnings"]); self.assertEqual(codes, ["empty_geometry", "hidden", "implausible_scale"])+    def test_no_model_and_exempt(self):+        self.write("D", ""); r = model_check.check_footprint(self.info, "Adom", "D"); self.assertEqual(r["warnings"][0]["code"], "no_model_reference")+        self.write("TestPoint_Pad", ""); r = model_check.check_footprint(self.info, "Adom", "TestPoint_Pad"); self.assertTrue(r["ok"]); self.assertIn("none expected", r["note"])+    def test_unresolved_variable(self):+        self.write("E", '(model "${NOPE}/x.step")'); r = model_check.check_footprint(self.info, "Adom", "E"); self.assertEqual(r["warnings"][0]["code"], "unresolved_variable")+    def test_board(self):+        d = Path(self.tmp.name); board = d / "b.kicad_pcb"+        board.write_text('(kicad_pcb (version 20240108) (generator "t") (footprint "Adom:A" (layer "F.Cu") (property "Reference" "U1") (model "${KICAD10_3DMODEL_DIR}/Adom.3dshapes/GOOD.step")) (footprint "Adom:B" (layer "F.Cu") (property "Reference" "U2") (model "BARE.step")) (footprint "TestPoint:TP" (layer "F.Cu") (property "Reference" "TP1")) (footprint "Adom:C" (layer "F.Cu") (property "Reference" "U3")))')+        r = model_check.check_board(self.info, str(board)); c = r["counts"]+        self.assertEqual((c["footprints"], c["withModel"], c["noModelExpected"], c["missingModel"], c["unresolvedModel"]), (4, 2, 1, 1, 1)); self.assertEqual([x["reference"] for x in r["footprintsWithWarnings"]], ["U2", "U3"])++if __name__ == "__main__":+    unittest.main()