from __future__ import annotations import json, tempfile, unittest from pathlib import Path from cadfs_to_cdsl.featurescript_parser import parse_featurescript from cadfs_to_cdsl.lowering import lower_model from cadfs_to_cdsl.pipeline import convert_one from cadfs_to_cdsl.dataset import Sample from cadfs_to_cdsl.dataset import scan_dataset from cadfs_to_cdsl.tests.test_parser import SOURCE, TRANSFORM_SOURCE class LoweringTests(unittest.TestCase): def test_missing_dataset_fails_explicitly(self): with tempfile.TemporaryDirectory() as tmp: with self.assertRaises(FileNotFoundError): scan_dataset(Path(tmp) / "missing") def test_circle_extrude_is_valid_cdsl_11(self): result = lower_model(parse_featurescript(SOURCE, "00000173"), {}) self.assertEqual(result.status, "converted_complete") self.assertEqual(result.cdsl["schema_version"], "1.1.0") self.assertEqual(result.cdsl["geometry"]["sketches"][0]["profile"]["radius_mm"], 9.53) from engine.cdsl_engine.semantic_validation import validate_semantic_cdsl self.assertTrue(validate_semantic_cdsl(result.cdsl)["future_rebuild_ready"]) def test_unsupported_operation_is_audited_not_invented(self): source = SOURCE.replace('extrude(context, id + "F1",', 'shell(context, id + "F1",') result = lower_model(parse_featurescript(source, "00000173"), {}) self.assertEqual(result.status, "deferred_no_executable_feature") self.assertIsNone(result.cdsl) self.assertEqual(result.diagnostics[0]["code"], "unsupported_operation") def test_direct_translation_transform_is_baked_into_the_source_feature(self): result = lower_model(parse_featurescript(TRANSFORM_SOURCE, "transform"), {}) self.assertEqual(result.status, "converted_complete") self.assertEqual([item["id"] for item in result.cdsl["features"]], ["f_F1"]) self.assertEqual(result.history[-1]["feature_id"], "F2") self.assertFalse(result.diagnostics) self.assertEqual(result.cdsl["geometry"]["sketches"][0]["workplane"]["origin_mm"], [10.0, 0.0, 0.0]) def test_copy_transform_remains_an_explicit_engine_capability_gap(self): source = TRANSFORM_SOURCE.replace('"makeCopy":false', '"makeCopy":true') result = lower_model(parse_featurescript(source, "transform-copy"), {}) self.assertEqual(result.status, "converted_partial") self.assertEqual(result.diagnostics[-1]["code"], "unsupported_engine_capability") self.assertEqual(result.diagnostics[-1]["capability"], "transform") def test_symmetric_cut_lowers_to_two_sided_cut(self): source = SOURCE.replace('"depth":120 * mm', '"operationType":NewBodyOperationType.REMOVE, "depth":120 * mm, "symmetric":true') result = lower_model(parse_featurescript(source, "symmetric-cut"), {}) self.assertEqual(result.status, "converted_complete") feature = result.cdsl["features"][0] self.assertEqual(feature["atomic_id"], "extrude_cut_two_sided") self.assertEqual(feature["params"]["distance_mm"], 60.0) self.assertEqual(feature["params"]["reverse_distance_mm"], 60.0) def test_through_all_extrudes_reuse_the_engine_extent_contract(self): for operation_type, expected_atomic in [ ("NewBodyOperationType.NEW", "extrude_add_blind"), ("NewBodyOperationType.REMOVE", "extrude_cut_blind"), ]: with self.subTest(operation_type=operation_type): source = SOURCE.replace( '"depth":120 * mm', f'"operationType":{operation_type}, "depth":120 * mm, "endBound":BoundingType.THROUGH_ALL', ) result = lower_model(parse_featurescript(source, f"through-all-{operation_type}"), {}) self.assertEqual(result.status, "converted_complete") feature = result.cdsl["features"][0] self.assertEqual(feature["atomic_id"], expected_atomic) self.assertEqual(feature["params"]["end_condition"]["type"], "through_all") def test_up_to_next_extrudes_reuse_the_engine_extent_contract(self): for operation_type, expected_atomic in [ ("NewBodyOperationType.NEW", "extrude_add_blind"), ("NewBodyOperationType.REMOVE", "extrude_cut_blind"), ]: with self.subTest(operation_type=operation_type): source = SOURCE.replace( '"depth":120 * mm', f'"operationType":{operation_type}, "depth":120 * mm, "endBound":BoundingType.UP_TO_NEXT', ) result = lower_model(parse_featurescript(source, f"up-to-next-{operation_type}"), {}) self.assertEqual(result.status, "converted_complete") feature = result.cdsl["features"][0] self.assertEqual(feature["atomic_id"], expected_atomic) self.assertEqual(feature["params"]["end_condition"]["type"], "through_next") def test_open_nonconstruction_geometry_is_not_silently_dropped(self): source = SOURCE.replace('skSolve(sketch);', 'skLineSegment(sketch, "open", {"start":v(0, 0) * mm, "end":v(20, 0) * mm}); skSolve(sketch);', 1) result = lower_model(parse_featurescript(source, "open-profile"), {}) self.assertIsNone(result.cdsl) self.assertEqual(result.diagnostics[0]["code"], "sketch_deferred") self.assertIn("open non-construction", result.diagnostics[0]["message"]) def test_surface_revolve_is_not_disguised_as_solid_revolve(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" feature = root / "featurescript_rp/0095/00957738.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), "00957738"), {}) gap = next(item for item in result.diagnostics if item.get("feature_id") == "F2") self.assertEqual(gap["capability"], "revolve_surface") self.assertIsNone(result.cdsl) def test_fit_spline_loft_lowers_to_executable_loft_add(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" feature = root / "featurescript_rp/0061/00612529.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), "00612529"), {}) self.assertIsNotNone(result.cdsl) features = {item["id"]: item for item in result.cdsl["features"]} self.assertEqual(features["f_F3"]["atomic_id"], "loft_add") self.assertEqual(features["f_F3"]["params"]["profile_sketch_ids"], ["sketch_F2", "sketch_F0"]) self.assertEqual(features["f_F4"]["atomic_id"], "pattern_mirror") self.assertEqual(features["f_F4"]["params"]["source_feature_ids"], ["f_F3"]) sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]} self.assertEqual(sketches["sketch_F0"]["profile"]["contours"][0]["segments"][0]["type"], "bspline") self.assertNotIn("loft", {item.get("operation") for item in result.diagnostics}) def test_fit_spline_loft_candidate_rebuilds(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" feature = root / "featurescript_rp/0061/00612529.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), "00612529"), {}) with tempfile.TemporaryDirectory() as tmp: from cadfs_to_cdsl.rebuild import rebuild_candidate rebuilt = rebuild_candidate(result.cdsl, Path(tmp) / "rebuilt.step") self.assertEqual(rebuilt["status"], "rebuilt") self.assertGreater(rebuilt["result"]["volume_mm3"], 0) def test_direct_translation_transform_updates_the_source_revolve(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" feature = root / "featurescript_rp/0011/00111611.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), "00111611"), {}) self.assertNotIn("F10", {item.get("feature_id") for item in result.diagnostics}) features = {item["id"]: item for item in result.cdsl["features"]} sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]} self.assertEqual(sketches[features["f_F9"]["sketch_id"]]["workplane"]["origin_mm"], [77.16, -11.67, -63.0]) for actual, expected in zip(features["f_F9"]["params"]["axis"]["origin_mm"], [0.13, -11.67, 56.11]): self.assertAlmostEqual(actual, expected) def test_circular_pattern_lowers_to_existing_engine_contract(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" feature = root / "featurescript_rp/0042/00423838.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), "00423838"), {}) self.assertIsNotNone(result.cdsl) features = {item["id"]: item for item in result.cdsl["features"]} pattern = features["f_F4"] self.assertEqual(pattern["atomic_id"], "pattern_circular") self.assertEqual(pattern["params"]["source_feature_ids"], ["f_F1"]) self.assertEqual(pattern["params"]["pattern_count"], 6) self.assertEqual(pattern["params"]["sweep_angle_deg"], 360.0) self.assertEqual(pattern["params"]["axis"]["direction"], [0.0, 0.0, -1.0]) self.assertNotIn("F2", {item.get("feature_id") for item in result.diagnostics}) sketches = {item["id"]: item for item in result.cdsl["geometry"]["sketches"]} self.assertNotEqual(sketches["sketch_F0"]["workplane"]["normal"], [0.0, 0.0, 1.0]) self.assertNotIn("circularPattern", {item.get("operation") for item in result.diagnostics}) def test_reference_plane_variants_lower_to_explicit_frames(self): root = Path(__file__).parents[2] / "data/cadfs-sample/CADFS_test" cases = { "00159804": {"F3", "F5"}, "00192744": {"F3"}, "00212904": {"F3"}, "00542223": {"F3"}, } for sample_id, expected in cases.items(): with self.subTest(sample_id=sample_id): feature = root / "featurescript_rp" / sample_id[:4] / f"{sample_id}.txt" if not feature.exists(): self.skipTest("CADFS sample is not installed") result = lower_model(parse_featurescript(feature.read_text(), sample_id), {}) features = {item["name"]: item for item in result.cdsl["features"]} for feature_id in expected: plane = features[feature_id] self.assertEqual(plane["atomic_id"], "reference_plane") self.assertEqual(set(plane["params"]["plane"]), {"origin_mm", "x_dir", "normal"}) diagnostics = [item for item in result.diagnostics if item.get("operation") == "cPlane"] self.assertFalse(diagnostics) def test_feature_face_profile_is_not_reused_as_original_sketch(self): source = SOURCE.replace('qSketchRegion(id + "F0", true)', 'makeQuery(id+"F1.opExtrude","CAP_FACE",FACE,{"isStart":false})') result = lower_model(parse_featurescript(source, "face-profile"), {}) self.assertIsNone(result.cdsl) self.assertEqual(result.diagnostics[0]["code"], "unsupported_engine_capability") self.assertIn("extrude_profile_topology:cap_face", result.diagnostics[0]["capability"]) def test_conversion_writes_status_and_sidecars(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp); source = root / "00000173.txt"; source.write_text(SOURCE) sample = Sample("00000173", {"featurescript": str(source)}, {"featurescript": "x"}) status = convert_one(sample, root / "out", force=True) directory = root / "out/samples/00000173" self.assertEqual(status["conversion_status"], "converted_complete") for name in ("candidate.cdsl.json", "history.json", "diagnostics.json", "status.json"): self.assertTrue((directory / name).exists(), name) self.assertEqual(json.loads((directory / "candidate.cdsl.json").read_text())["part_id"], "00000173") if __name__ == "__main__": unittest.main()