"""Contract tests for the SimpleCADAPI 2.0 rearchitecture. These tests intentionally focus on stable public behavior and on a small number of future-facing expression contracts. The future-facing tests are skipped until the corresponding 2.0 APIs exist. """ from __future__ import annotations import unittest import simplecadapi as scad from simplecadapi.graph import GraphSession REQUIRED_GEOMETRY_TYPES = ( "Vertex", "Edge", "Wire", "Face", "Solid", ) REQUIRED_RSTYLE_APIS = ( "make_point_rvertex", "make_line_redge", "make_rectangle_rface", "make_box_rsolid", "make_cylinder_rsolid", "extrude_rsolid", "revolve_rsolid", "fillet_rsolid", "chamfer_rsolid", "shell_rsolid", "cut_rsolid", "union_rsolid", "intersect_rsolid", ) class TestRearchitecture20ApiContracts(unittest.TestCase): """Contracts derived from REQ-API-* in the 2.0 requirements doc.""" def test_shape_first_geometry_types_remain_public(self): for type_name in REQUIRED_GEOMETRY_TYPES: with self.subTest(type_name=type_name): self.assertTrue(hasattr(scad, type_name)) def test_rstyle_modeling_api_remains_public(self): for name in REQUIRED_RSTYLE_APIS: with self.subTest(name=name): self.assertTrue(hasattr(scad, name)) def test_constant_only_modeling_requires_no_expression_wrapper(self): box = scad.make_box_rsolid(4.0, 5.0, 6.0) self.assertIsInstance(box, scad.Solid) self.assertGreater(box.get_volume(), 0.0) def test_shape_operations_remain_type_closed_for_core_workflow(self): face = scad.make_rectangle_rface(2.0, 1.0) self.assertIsInstance(face, scad.Face) solid = scad.extrude_rsolid(face, (0, 0, 1), 3.0) self.assertIsInstance(solid, scad.Solid) moved = scad.translate_shape(solid, (1.0, 2.0, 3.0)) self.assertIsInstance(moved, scad.Solid) rotated = scad.rotate_shape(moved, 30.0, (0, 0, 1)) self.assertIsInstance(rotated, scad.Solid) def test_boolean_pipeline_keeps_returning_solids(self): body = scad.make_box_rsolid(4.0, 4.0, 4.0) tool = scad.make_cylinder_rsolid(0.75, 6.0, bottom_face_center=(0.0, 0.0, -1.0)) result = scad.cut_rsolid(body, tool) self.assertIsInstance(result, scad.Solid) class TestRearchitecture20ExpressionContracts(unittest.TestCase): """Future contracts derived from REQ-EXPR-* in the 2.0 requirements doc.""" def test_explicit_variable_api_exists(self): var = scad.var("r", 10.0) self.assertIsNotNone(var) self.assertIsInstance(var, scad.Var) def test_variable_name_is_explicit_and_preserved(self): var = scad.var("radius", 10.0) self.assertEqual(var.name, "radius") def test_expression_supports_standard_arithmetic(self): r = scad.var("r", 10.0) expr = ((r + 2) * 3 - 4) / 2 self.assertIsInstance(expr, scad.Expr) self.assertAlmostEqual(float(expr), 16.0) def test_expression_system_is_unitless_phase_one(self): r = scad.var("r", 10.0) self.assertFalse(hasattr(r, "unit")) def test_expression_values_can_flow_into_public_modeling_apis(self): r = scad.var("r", 10.0) face = scad.make_circle_rface((0, 0, 0), r) self.assertIsInstance(face, scad.Face) solid = scad.extrude_rsolid(face, (0, 0, 1), r * 2) self.assertIsInstance(solid, scad.Solid) def test_expression_graph_is_public_and_round_trippable(self): graph = scad.ExpressionGraph() r = scad.var("r", 10.0) graph.register(r * 2 + 1) payload = graph.to_dict() rebuilt = scad.ExpressionGraph.from_dict(payload) self.assertGreaterEqual(rebuilt.node_count, 4) expr_nodes = [node for node in payload["nodes"] if node["kind"] == "expr"] self.assertGreaterEqual(len(expr_nodes), 2) def test_graph_session_tracks_expression_graph_separately(self): r = scad.var("r", 5.0) with GraphSession() as session: face = scad.make_circle_rface((0, 0, 0), r) solid = scad.extrude_rsolid(face, (0, 0, 1), r * 2) self.assertIsInstance(solid, scad.Solid) self.assertGreaterEqual(session.graph.node_count, 2) self.assertGreaterEqual(session.expression_graph.node_count, 3) leaf = session.graph.leaf_nodes()[0] self.assertIn("distance", leaf.param_exprs) self.assertIn("expr_id", leaf.param_exprs["distance"]) def test_session_export_includes_expression_graph(self): r = scad.var("r", 3.0) with GraphSession() as session: scad.make_circle_rface((0, 0, 0), r) payload = scad.import_session_json(scad.export_session_json(session)) self.assertIn("graph", payload) self.assertIn("expression_graph", payload) self.assertGreaterEqual(payload["expression_graph"].node_count, 1) class TestRearchitecture20GeometryContracts(unittest.TestCase): def test_geometry_layer_remains_shape_first_not_scene_graph_first(self): box = scad.make_box_rsolid(1.0, 2.0, 3.0) self.assertFalse(hasattr(box, "children_nodes")) self.assertFalse(hasattr(box, "scene_transform")) def test_sketch_can_exist_without_replacing_shape_hierarchy(self): self.assertTrue(hasattr(scad, "Sketch")) self.assertTrue(hasattr(scad, "Face")) self.assertTrue(hasattr(scad, "Solid")) sketch = scad.Sketch() self.assertIsInstance(sketch, scad.Sketch) class TestRearchitecture20KernelContracts(unittest.TestCase): def test_geometry_wrappers_accept_raw_occ_shapes_and_expose_wrapped_storage(self): solid = scad.make_box_rsolid(1.0, 2.0, 3.0) clone = scad.Solid(solid.wrapped) self.assertIsInstance(clone, scad.Solid) self.assertTrue(hasattr(clone, "wrapped")) self.assertFalse(hasattr(clone, "cq_solid")) def test_public_modeling_path_produces_wrapped_storage_on_shapes(self): face = scad.make_circle_rface((0, 0, 0), 2.0) solid = scad.extrude_rsolid(face, (0, 0, 1), 3.0) self.assertTrue(hasattr(face, "wrapped")) self.assertTrue(hasattr(solid, "wrapped")) class TestRearchitecture20HistoryContracts(unittest.TestCase): def test_operation_graph_is_still_recorded_for_public_modeling_calls(self): with GraphSession() as session: body = scad.make_box_rsolid(4.0, 4.0, 4.0) tool = scad.make_cylinder_rsolid( 0.75, 6.0, bottom_face_center=(0.0, 0.0, -1.0) ) result = scad.cut_rsolid(body, tool) payload = scad.import_model_json(scad.export_model_json(session)) self.assertIsInstance(result, scad.Solid) self.assertGreaterEqual(session.graph.node_count, 3) allowed_ops = set(payload["canonical_contract"]["core_op_set"]) self.assertTrue(all(node.op in allowed_ops for node in session.graph.nodes)) def test_semantic_delta_exists_alongside_topology_delta(self): delta = scad.SemanticDelta() self.assertTrue(hasattr(delta, "created")) self.assertTrue(hasattr(delta, "modified")) self.assertTrue(hasattr(delta, "deleted")) def test_topology_delta_types_still_exposed_in_data_model(self): from simplecadapi.topology import TopoDelta delta = TopoDelta() self.assertTrue(hasattr(delta, "preserved")) self.assertTrue(hasattr(delta, "modified")) self.assertTrue(hasattr(delta, "generated")) self.assertTrue(hasattr(delta, "deleted")) def test_semantic_ref_exists_beside_topo_ref(self): ref = scad.SemanticRef( graph_id="g0", node_id="n0", entity_type="Sketch", entity_id="sk0" ) self.assertEqual(ref.entity_type, "Sketch") class TestRearchitecture20AssemblyContracts(unittest.TestCase): def test_single_body_part_assembly_mvp_public_surface_is_exposed(self): self.assertTrue(hasattr(scad, "Assembly")) self.assertTrue(hasattr(scad, "Part")) self.assertTrue(hasattr(scad, "Material")) self.assertTrue(hasattr(scad, "Placement")) self.assertTrue(hasattr(scad, "make_assembly_rassembly")) self.assertTrue(hasattr(scad, "make_part_rpart")) self.assertTrue(hasattr(scad, "make_material_rmaterial")) self.assertTrue(hasattr(scad, "make_placement_rplacement")) self.assertTrue(hasattr(scad, "add_component_rassembly")) self.assertTrue(hasattr(scad, "place_component_rassembly")) self.assertTrue(hasattr(scad, "make_compound_from_assembly_rcompound")) self.assertFalse(hasattr(scad, "PartHandle")) self.assertFalse(hasattr(scad, "PointAnchor")) self.assertFalse(hasattr(scad, "AxisAnchor")) self.assertFalse(hasattr(scad, "AssemblyResult")) self.assertFalse(hasattr(scad, "SolveReport")) self.assertFalse(hasattr(scad, "clone_assembly_rassembly")) self.assertFalse(hasattr(scad, "add_part_rassembly")) self.assertFalse(hasattr(scad, "translate_part_rassembly")) self.assertFalse(hasattr(scad, "rotate_part_rassembly")) self.assertFalse(hasattr(scad, "solve_assembly_rresult")) self.assertFalse(hasattr(scad, "constrain_offset_rassembly")) self.assertFalse(hasattr(scad, "constrain_concentric_rassembly")) self.assertFalse(hasattr(scad, "constrain_distance_rassembly")) self.assertFalse(hasattr(scad, "stack_rassembly")) def test_model_json_export_has_no_assembly_keyword(self): with GraphSession() as session: scad.make_box_rsolid(1.0, 1.0, 1.0) with self.assertRaises(TypeError): scad.export_model_json(session, assembly=object()) class TestRearchitecture20IoContracts(unittest.TestCase): def test_session_export_becomes_canonical_json_seed(self): r = scad.var("r", 2.0) with GraphSession() as session: scad.make_circle_rface((0, 0, 0), r) payload = scad.import_session_json(scad.export_session_json(session)) self.assertGreaterEqual(payload["graph"].node_count, 1) self.assertGreaterEqual(payload["expression_graph"].node_count, 1) def test_step_export_still_exists_as_final_geometry_export(self): self.assertTrue(hasattr(scad, "export_step")) self.assertTrue(hasattr(scad, "export_stl")) def test_model_json_export_exists_as_canonical_seed(self): r = scad.var("r", 2.0) with GraphSession() as session: scad.make_circle_rface((0, 0, 0), r) payload = scad.import_model_json(scad.export_model_json(session)) self.assertIn("graph", payload) self.assertIn("expression_graph", payload) def test_model_json_declares_final_state_canonical_contract(self): with GraphSession() as session: scad.make_box_rsolid(1.0, 2.0, 3.0) payload = scad.import_model_json(scad.export_model_json(session)) self.assertIn("canonical_contract", payload) contract = payload["canonical_contract"] self.assertEqual(contract["contract_version"], "2.0") self.assertEqual(contract["graph_roles"]["graph"], "canonical_low_level_graph") self.assertEqual(contract["graph_roles"]["leaf_ids"], "explicit_result_set") self.assertEqual(contract["replay_policy"]["preferred_graph"], "graph") self.assertEqual(contract["replay_policy"]["default_mode"], "strict") self.assertEqual( contract["replay_policy"]["permissive_mode"], "explicit_opt_in" ) def test_model_json_declares_selection_ref_resolution_order(self): with GraphSession() as session: box = scad.make_box_rsolid(4.0, 4.0, 4.0) scad.fillet_rsolid(box, [box.get_edges(i) for i in range(2)], 0.2) payload = scad.import_model_json(scad.export_model_json(session)) selection_schema = payload["canonical_contract"]["selection_ref_schema"] self.assertEqual( selection_schema["replay_resolution_order"], [ "geo_select_nodes", "selection_query", "explicit_topo_refs", "stable_indices", "selector_hint", ], )