from __future__ import annotations import unittest from cadfs_to_cdsl.featurescript_lexer import lex from cadfs_to_cdsl.featurescript_parser import parse_featurescript from cadfs_to_cdsl.ir import Call from cadfs_to_cdsl.lowering import _number, _queries, _source_refs from cadfs_to_cdsl.query_parser import parse_query, query_expr from cadfs_to_cdsl.units import length_mm SOURCE = r''' FeatureScript 1511; export const f = defineFeature(function(context, id, definition) { { var Q0; Q0=qCreatedBy(makeId("Top.planeOp"), FACE); var sketch = newSketch(context, id + "F0", {"sketchPlane":qUnion([Q0])}); skCircle(sketch, "E0", {"center":v(0, 0) * mm, "radius":9.53 * mm}); skSolve(sketch); } { var Q0; Q0=qSketchRegion(id + "F0", true); extrude(context, id + "F1", {"entities":qUnion([Q0]), "depth":120 * mm}); } }); ''' TRANSFORM_SOURCE = SOURCE.replace( '\n});\n', ''' transform(context, id + "F2", {"entities":qCreatedBy(id + "F1", BODY), "transformType":TransformType.TRANSLATION_3D, "dx":10 * mm, "dy":0 * mm, "dz":0 * mm, "makeCopy":false}); }); ''', ) UNKNOWN_FEATURE_SOURCE = SOURCE.replace( '\n});\n', ''' assignVariable(context, id + "F2", {"name" : "diameter", "value" : 10 * mm}); }); ''', ) class ParserTests(unittest.TestCase): def test_lexer_ignores_comments_and_preserves_lines(self): tokens = lex('// a\nfoo(/*b*/"x")') self.assertEqual([token.value for token in tokens[:-1]], ["foo", "(", '"x"', ")"]) self.assertEqual(tokens[0].line, 2) def test_nested_feature_script(self): model = parse_featurescript(SOURCE, "00000173") self.assertEqual([step.feature_id for step in model.steps], ["F0", "F1"]) self.assertEqual(model.sketches[0].entities[0].operation, "skCircle") self.assertIsInstance(model.features[0].params["entities"], Call) def test_transform_is_preserved_as_a_feature(self): model = parse_featurescript(TRANSFORM_SOURCE, "transform") self.assertEqual([step.feature_id for step in model.steps], ["F0", "F1", "F2"]) self.assertEqual(model.features[-1].operation, "transform") self.assertEqual(model.features[-1].params["transformType"], "TransformType.TRANSLATION_3D") def test_unknown_direct_context_feature_is_preserved_for_capability_diagnostics(self): model = parse_featurescript(UNKNOWN_FEATURE_SOURCE, "unknown-feature") self.assertEqual([(feature.feature_id, feature.operation) for feature in model.features], [("F1", "extrude"), ("F2", "assignVariable")]) def test_query_parser(self): query = Call("makeQuery", [Call("__binary__", ["id", "+", "F1.opExtrude"]), "CAP_EDGE", "EDGE", {"isStart": False, "x": Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"])}]) value = parse_query(query) self.assertEqual((value.owner_feature, value.source_sketch, value.source_entity), ("F1", "F0", "E0")) self.assertFalse(value.is_start) self.assertEqual(value.ast["call"], "makeQuery") self.assertEqual(value.ast["args"][0]["call"], "__binary__") def test_signed_parenthesized_scalar_preserves_unit_expression_semantics(self): model = parse_featurescript(SOURCE.replace( '"depth":120 * mm', '"depth":-(10 + 2) / 2 * mm', 1, ), "signed-scalar") depth = model.features[-1].params["depth"] self.assertEqual((depth.name, depth.args[1]), ("__binary__", "*")) self.assertEqual(_number(depth), -6.0) def test_query_expression_preserves_nested_set_and_filter_boundaries(self): source = Call("qUnion", [[ Call("qConstructionFilter", [ Call("qBodyType", [ Call("qCreatedBy", [Call("__binary__", ["id", "+", "F1"]), "EDGE"]), "BodyType.WIRE", ]), "ConstructionObject.NO", ]), Call("qSubtraction", [ Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E0"]), Call("sQuery", [Call("__binary__", ["id", "+", "F0.wireOp"]), "EDGE", "E1"]), ]), ]]) expression = query_expr(source) self.assertEqual(expression["version"], "1.0") self.assertEqual(expression["root"]["node"], "set") self.assertEqual(expression["root"]["operator"], "union") filtered, subtraction = expression["root"]["operands"] self.assertEqual((filtered["node"], filtered["filter"]), ("filter", "construction")) self.assertEqual((filtered["input"]["node"], filtered["input"]["filter"]), ("filter", "body_type")) self.assertEqual((subtraction["node"], subtraction["operator"]), ("set", "subtraction")) parsed = parse_query(source) self.assertEqual(parsed.query_combinators, ["qUnion", "qSubtraction"]) self.assertEqual( parsed.filters, ["qConstructionFilter", "qBodyType"], ) def test_source_version_and_standard_library_are_retained(self): source = '''FeatureScript 1511; import(path : "onshape/std/geometry.fs", version : "1511.0"); import(version : "1511.0", path : "onshape/std/common.fs"); export const f = defineFeature(function(context, id, definition) {});''' model = parse_featurescript(source, "versioned") self.assertEqual(model.featurescript_version, "1511") self.assertEqual(model.standard_library, "onshape/std/geometry.fs") self.assertEqual(model.standard_library_version, "1511.0") self.assertEqual(model.standard_library_imports, [ {"path": "onshape/std/geometry.fs", "version": "1511.0"}, {"path": "onshape/std/common.fs", "version": "1511.0"}, ]) def test_block_scoped_query_aliases_do_not_use_the_last_assignment(self): source = r''' { var Q0; { var subQ0=sQuery(id + "F2.wireOp", EDGE, "E2"); Q0=makeQuery(id + "F4.opExtrude", "SWEPT_FACE", FACE, {"derivedFrom":subQ0}); } var Q1; { var subQ0=sQuery(id + "F2.wireOp", EDGE, "E4"); Q1=makeQuery(id + "F4.opExtrude", "SWEPT_FACE", FACE, {"derivedFrom":subQ0}); } chamfer(context, id + "F5", {"entities":qUnion([Q0, Q1]), "width":5 * mm}); } ''' model = parse_featurescript(source, "scoped-query") queries = _queries(model.features[0].params["entities"]) self.assertEqual([_source_refs(query)[0] for query in queries], [("F2", "E2"), ("F2", "E4")]) def test_safe_units(self): self.assertEqual(length_mm("2 * inch"), 50.8) self.assertEqual(length_mm("25.4 / 2 * mm"), 12.7) with self.assertRaises(ValueError): length_mm("external.value * mm") if __name__ == "__main__": unittest.main()