mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
Add ifcmcp: MCP server for IFC model querying and editing (#7847)
ifcmcp is a new Model Context Protocol server that wraps ifcquery and ifcedit, holding an IFC model in memory across tool calls. It is the preferred way to interact with IFC models from AI assistants and MCP-compatible clients. Setup: claude mcp add --transport stdio ifc -- python3 -m ifcmcp Session tools: ifc_load, ifc_save Query tools: ifc_summary, ifc_tree, ifc_info, ifc_select, ifc_relations, ifc_clash, ifc_validate, ifc_schedule, ifc_cost, ifc_schema, ifc_contexts, ifc_materials, ifc_plot, ifc_render, ifc_shape, ifc_shape_list, ifc_shape_docs Edit discovery: ifc_list, ifc_docs Edit execution: ifc_edit, ifc_quantify The model stays in memory between calls - ifc_edit does not auto-save; call ifc_save explicitly when done. Depends on both ifcquery and ifcedit Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
@@ -0,0 +1,59 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.api.owner.settings
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import pytest
|
||||
|
||||
from ifcmcp.core import IfcSession
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
return IfcSession()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model():
|
||||
"""IFC4 model with a spatial hierarchy, a wall, and a slab."""
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
|
||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
|
||||
ifcopenshell.api.unit.assign_unit(f)
|
||||
|
||||
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
|
||||
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
|
||||
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
||||
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
||||
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
||||
|
||||
slab = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSlab", name="Slab001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[slab], relating_structure=storey)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_file(model, tmp_path):
|
||||
"""Write the model fixture to a temp file and return the path."""
|
||||
path = tmp_path / "test.ifc"
|
||||
model.write(str(path))
|
||||
return str(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loaded_session(model):
|
||||
"""An IfcSession with an in-memory model already loaded (no file path)."""
|
||||
s = IfcSession()
|
||||
s.model = model
|
||||
return s
|
||||
@@ -0,0 +1,96 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import json
|
||||
|
||||
import ifcopenshell
|
||||
import pytest
|
||||
|
||||
from ifcmcp.core import IfcSession, IfcSessionError
|
||||
|
||||
|
||||
class TestNoModel:
|
||||
def test_edit_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_edit("root.create_entity")
|
||||
|
||||
|
||||
class TestList:
|
||||
def test_list_all_modules(self, loaded_session):
|
||||
result = loaded_session.ifc_list()
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
modules = [m["module"] for m in result]
|
||||
assert "root" in modules
|
||||
assert "spatial" in modules
|
||||
|
||||
def test_list_module_functions(self, loaded_session):
|
||||
result = loaded_session.ifc_list(module="root")
|
||||
assert isinstance(result, list)
|
||||
names = [f["name"] for f in result]
|
||||
assert "create_entity" in names
|
||||
|
||||
def test_list_empty_string_returns_modules(self, loaded_session):
|
||||
result = loaded_session.ifc_list(module="")
|
||||
assert isinstance(result, list)
|
||||
assert any(m["module"] == "root" for m in result)
|
||||
|
||||
|
||||
class TestDocs:
|
||||
def test_docs_create_entity(self, loaded_session):
|
||||
result = loaded_session.ifc_docs("root.create_entity")
|
||||
assert result["module"] == "root"
|
||||
assert result["function"] == "create_entity"
|
||||
assert "params" in result
|
||||
|
||||
def test_docs_bad_format(self, loaded_session):
|
||||
with pytest.raises(ValueError):
|
||||
loaded_session.ifc_docs("no_dot_here")
|
||||
|
||||
|
||||
class TestEdit:
|
||||
def test_create_entity(self, loaded_session):
|
||||
result = loaded_session.ifc_edit("root.create_entity", json.dumps({"ifc_class": "IfcWall", "name": "NewWall"}))
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcWall"
|
||||
assert result["result"]["name"] == "NewWall"
|
||||
|
||||
def test_create_entity_default_params(self, loaded_session):
|
||||
result = loaded_session.ifc_edit("root.create_entity", "{}")
|
||||
assert result["ok"] is True
|
||||
|
||||
def test_unknown_function(self, loaded_session):
|
||||
result = loaded_session.ifc_edit("root.nonexistent", "{}")
|
||||
assert result["ok"] is False
|
||||
assert "Cannot find" in result["error"]
|
||||
|
||||
def test_unknown_parameter(self, loaded_session):
|
||||
result = loaded_session.ifc_edit("root.create_entity", json.dumps({"bogus": "value"}))
|
||||
assert result["ok"] is False
|
||||
assert "Unknown parameter" in result["error"]
|
||||
|
||||
def test_bad_json(self, loaded_session):
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
loaded_session.ifc_edit("root.create_entity", "not json")
|
||||
|
||||
def test_edit_does_not_save(self, loaded_session, tmp_path):
|
||||
"""Verify that ifc_edit mutates the in-memory model but does not write to disk."""
|
||||
path = str(tmp_path / "test.ifc")
|
||||
loaded_session.model.write(path)
|
||||
loaded_session.model_path = path
|
||||
|
||||
before_count = sum(1 for _ in loaded_session.model)
|
||||
loaded_session.ifc_edit("root.create_entity", json.dumps({"ifc_class": "IfcWall", "name": "Unsaved"}))
|
||||
after_count = sum(1 for _ in loaded_session.model)
|
||||
assert after_count == before_count + 1
|
||||
|
||||
on_disk = ifcopenshell.open(path)
|
||||
disk_count = sum(1 for _ in on_disk)
|
||||
assert disk_count == before_count
|
||||
|
||||
def test_assign_container(self, loaded_session):
|
||||
wall = loaded_session.model.by_type("IfcWall")[0]
|
||||
storey = loaded_session.model.by_type("IfcBuildingStorey")[0]
|
||||
result = loaded_session.ifc_edit(
|
||||
"spatial.assign_container",
|
||||
json.dumps({"products": str(wall.id()), "relating_structure": str(storey.id())}),
|
||||
)
|
||||
assert result["ok"] is True
|
||||
@@ -0,0 +1,113 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import pytest
|
||||
|
||||
from ifcmcp.core import IfcSessionError
|
||||
|
||||
|
||||
class TestNoModel:
|
||||
"""All query tools should fail when no model is loaded."""
|
||||
|
||||
def test_summary_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_summary()
|
||||
|
||||
def test_tree_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_tree()
|
||||
|
||||
def test_info_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_info(1)
|
||||
|
||||
def test_select_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_select("IfcWall")
|
||||
|
||||
def test_relations_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_relations(1)
|
||||
|
||||
|
||||
class TestSummary:
|
||||
def test_schema(self, loaded_session):
|
||||
result = loaded_session.ifc_summary()
|
||||
assert result["schema"] == "IFC4"
|
||||
|
||||
def test_total_entities(self, loaded_session):
|
||||
result = loaded_session.ifc_summary()
|
||||
assert result["total_entities"] > 0
|
||||
|
||||
def test_project_name(self, loaded_session):
|
||||
result = loaded_session.ifc_summary()
|
||||
assert result["project"]["name"] == "TestProject"
|
||||
|
||||
def test_type_counts(self, loaded_session):
|
||||
result = loaded_session.ifc_summary()
|
||||
assert result["types"]["IfcWall"] == 1
|
||||
assert result["types"]["IfcSlab"] == 1
|
||||
|
||||
|
||||
class TestTree:
|
||||
def test_root_is_project(self, loaded_session):
|
||||
result = loaded_session.ifc_tree()
|
||||
assert result["type"] == "IfcProject"
|
||||
assert result["name"] == "TestProject"
|
||||
|
||||
def test_hierarchy_depth(self, loaded_session):
|
||||
result = loaded_session.ifc_tree()
|
||||
site = result["children"][0]
|
||||
assert site["type"] == "IfcSite"
|
||||
building = site["children"][0]
|
||||
assert building["type"] == "IfcBuilding"
|
||||
storey = building["children"][0]
|
||||
assert storey["type"] == "IfcBuildingStorey"
|
||||
|
||||
|
||||
class TestInfo:
|
||||
def test_wall_info(self, loaded_session):
|
||||
wall = loaded_session.model.by_type("IfcWall")[0]
|
||||
result = loaded_session.ifc_info(wall.id())
|
||||
assert result["id"] == wall.id()
|
||||
assert result["type"] == "IfcWall"
|
||||
|
||||
def test_invalid_id(self, loaded_session):
|
||||
with pytest.raises(Exception):
|
||||
loaded_session.ifc_info(999999)
|
||||
|
||||
|
||||
class TestSelect:
|
||||
def test_select_walls(self, loaded_session):
|
||||
result = loaded_session.ifc_select("IfcWall")
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "IfcWall"
|
||||
assert result[0]["name"] == "Wall001"
|
||||
|
||||
def test_select_slabs(self, loaded_session):
|
||||
result = loaded_session.ifc_select("IfcSlab")
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "Slab001"
|
||||
|
||||
def test_select_no_match(self, loaded_session):
|
||||
result = loaded_session.ifc_select("IfcWindow")
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestRelations:
|
||||
def test_wall_relations(self, loaded_session):
|
||||
wall = loaded_session.model.by_type("IfcWall")[0]
|
||||
result = loaded_session.ifc_relations(wall.id())
|
||||
assert result["id"] == wall.id()
|
||||
assert result["type"] == "IfcWall"
|
||||
assert "hierarchy" in result
|
||||
|
||||
def test_traverse_up(self, loaded_session):
|
||||
wall = loaded_session.model.by_type("IfcWall")[0]
|
||||
result = loaded_session.ifc_relations(wall.id(), traverse="up")
|
||||
assert isinstance(result, list)
|
||||
assert result[0]["type"] == "IfcWall"
|
||||
assert result[-1]["type"] == "IfcProject"
|
||||
|
||||
def test_traverse_empty_string_means_no_traverse(self, loaded_session):
|
||||
wall = loaded_session.model.by_type("IfcWall")[0]
|
||||
result = loaded_session.ifc_relations(wall.id(), traverse="")
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,106 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ifcmcp.server import build_server
|
||||
|
||||
|
||||
class TestServerRegistration:
|
||||
def test_server_name(self):
|
||||
server = build_server()
|
||||
assert server.name == "ifc-mcp"
|
||||
|
||||
def test_all_tools_registered(self):
|
||||
server = build_server()
|
||||
tools = [t.name for t in server._tool_manager.list_tools()]
|
||||
expected = [
|
||||
"ifc_load",
|
||||
"ifc_save",
|
||||
"ifc_summary",
|
||||
"ifc_tree",
|
||||
"ifc_info",
|
||||
"ifc_select",
|
||||
"ifc_relations",
|
||||
"ifc_clash",
|
||||
"ifc_list",
|
||||
"ifc_docs",
|
||||
"ifc_edit",
|
||||
]
|
||||
for name in expected:
|
||||
assert name in tools, f"Tool {name} not registered"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tool_fns():
|
||||
"""Return a dict of tool name → raw function from a freshly built server."""
|
||||
server = build_server()
|
||||
return {t.name: t.fn for t in server._tool_manager.list_tools()}
|
||||
|
||||
|
||||
PNG_FAKE = b"\x89PNG\r\n\x1a\nFAKE"
|
||||
SVG_FAKE = b"<svg>FAKE</svg>"
|
||||
|
||||
|
||||
class TestRenderOutputPath:
|
||||
def test_no_output_path_no_file_written(self, tool_fns, tmp_path):
|
||||
with patch("ifcmcp.core.IfcSession.ifc_render", return_value=PNG_FAKE):
|
||||
tool_fns["ifc_render"](selector="", element_ids=None, view="iso", output_path="")
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
def test_png_output_path_writes_file(self, tool_fns, tmp_path):
|
||||
out = str(tmp_path / "render.png")
|
||||
with patch("ifcmcp.core.IfcSession.ifc_render", return_value=PNG_FAKE):
|
||||
tool_fns["ifc_render"](selector="", element_ids=None, view="iso", output_path=out)
|
||||
assert open(out, "rb").read() == PNG_FAKE
|
||||
|
||||
|
||||
class TestPlotOutputPath:
|
||||
def test_no_output_path_no_file_written(self, tool_fns, tmp_path):
|
||||
with patch("ifcmcp.core.IfcSession.ifc_plot", return_value=PNG_FAKE):
|
||||
tool_fns["ifc_plot"](
|
||||
selector="",
|
||||
element_ids=None,
|
||||
view="floorplan",
|
||||
width_mm=297.0,
|
||||
height_mm=420.0,
|
||||
scale=0.01,
|
||||
png_width=1024,
|
||||
png_height=1024,
|
||||
output_path="",
|
||||
)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
def test_png_output_path_writes_png(self, tool_fns, tmp_path):
|
||||
out = str(tmp_path / "plot.png")
|
||||
with patch("ifcmcp.core.IfcSession.ifc_plot", return_value=PNG_FAKE):
|
||||
tool_fns["ifc_plot"](
|
||||
selector="",
|
||||
element_ids=None,
|
||||
view="floorplan",
|
||||
width_mm=297.0,
|
||||
height_mm=420.0,
|
||||
scale=0.01,
|
||||
png_width=1024,
|
||||
png_height=1024,
|
||||
output_path=out,
|
||||
)
|
||||
assert open(out, "rb").read() == PNG_FAKE
|
||||
|
||||
def test_svg_output_path_writes_svg(self, tool_fns, tmp_path):
|
||||
out = str(tmp_path / "plot.svg")
|
||||
# ifc_plot is called twice: once with "png" for the inline image,
|
||||
# once with "svg" for the file.
|
||||
with patch("ifcmcp.core.IfcSession.ifc_plot", side_effect=[PNG_FAKE, SVG_FAKE]):
|
||||
tool_fns["ifc_plot"](
|
||||
selector="",
|
||||
element_ids=None,
|
||||
view="floorplan",
|
||||
width_mm=297.0,
|
||||
height_mm=420.0,
|
||||
scale=0.01,
|
||||
png_width=1024,
|
||||
png_height=1024,
|
||||
output_path=out,
|
||||
)
|
||||
assert open(out, "rb").read() == SVG_FAKE
|
||||
@@ -0,0 +1,62 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
from unittest.mock import patch
|
||||
|
||||
import ifcopenshell
|
||||
import pytest
|
||||
|
||||
from ifcmcp.core import IfcSession, IfcSessionError
|
||||
|
||||
|
||||
class TestLoad:
|
||||
def test_load_file(self, session, model_file):
|
||||
result = session.ifc_load(model_file)
|
||||
assert "IFC4" in result
|
||||
assert session.model is not None
|
||||
assert session.model_path == model_file
|
||||
|
||||
def test_load_sets_entity_count(self, session, model_file):
|
||||
result = session.ifc_load(model_file)
|
||||
assert "entities" in result
|
||||
|
||||
def test_load_nonexistent_file(self, session):
|
||||
with pytest.raises(Exception):
|
||||
session.ifc_load("/nonexistent/path/model.ifc")
|
||||
|
||||
|
||||
class TestSave:
|
||||
def test_save_no_model(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_save()
|
||||
|
||||
def test_save_overwrites_original(self, session, model_file):
|
||||
session.ifc_load(model_file)
|
||||
result = session.ifc_save()
|
||||
assert model_file in result
|
||||
|
||||
def test_save_to_new_path(self, session, model_file, tmp_path):
|
||||
session.ifc_load(model_file)
|
||||
new_path = str(tmp_path / "output.ifc")
|
||||
result = session.ifc_save(new_path)
|
||||
assert new_path in result
|
||||
reloaded = ifcopenshell.open(new_path)
|
||||
assert reloaded.schema == "IFC4"
|
||||
|
||||
def test_save_no_path_no_original(self, loaded_session):
|
||||
with pytest.raises(IfcSessionError, match="No path specified"):
|
||||
loaded_session.ifc_save()
|
||||
|
||||
|
||||
class TestIfcPlotOutputFormat:
|
||||
"""ifc_plot should pass output_format through to the underlying plot function."""
|
||||
|
||||
def test_default_output_format_is_png(self, loaded_session):
|
||||
with patch("ifcmcp.core.plot_mod.plot", return_value=b"PNG_FAKE") as mock_plot:
|
||||
loaded_session.ifc_plot()
|
||||
mock_plot.assert_called_once()
|
||||
assert mock_plot.call_args.kwargs["output_format"] == "png"
|
||||
|
||||
def test_svg_output_format(self, loaded_session):
|
||||
with patch("ifcmcp.core.plot_mod.plot", return_value=b"SVG_FAKE") as mock_plot:
|
||||
result = loaded_session.ifc_plot(output_format="svg")
|
||||
assert result == b"SVG_FAKE"
|
||||
assert mock_plot.call_args.kwargs["output_format"] == "svg"
|
||||
@@ -0,0 +1,141 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from ifcmcp.core import IfcSessionError
|
||||
|
||||
|
||||
class TestShapeList:
|
||||
def test_returns_list(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_list()
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_has_expected_methods(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_list()
|
||||
names = [m["method"] for m in result]
|
||||
assert "polyline" in names
|
||||
assert "rectangle" in names
|
||||
assert "extrude" in names
|
||||
assert "profile" in names
|
||||
assert "get_representation" in names
|
||||
|
||||
def test_well_documented_methods_have_descriptions(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_list()
|
||||
by_name = {m["method"]: m for m in result}
|
||||
# These methods have detailed docstrings
|
||||
for name in ("polyline", "extrude", "rectangle", "profile", "get_representation"):
|
||||
assert by_name[name]["description"], f"'{name}' has no description"
|
||||
|
||||
def test_no_private_methods(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_list()
|
||||
assert not any(m["method"].startswith("_") for m in result)
|
||||
|
||||
def test_does_not_require_model(self, session):
|
||||
# ifc_shape_list is pure introspection — no model needed
|
||||
result = session.ifc_shape_list()
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
class TestShapeDocs:
|
||||
def test_extrude_docs(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_docs("extrude")
|
||||
assert result["method"] == "extrude"
|
||||
assert result["description"]
|
||||
assert "params" in result
|
||||
param_names = [p["name"] for p in result["params"]]
|
||||
assert "profile_or_curve" in param_names
|
||||
assert "magnitude" in param_names
|
||||
|
||||
def test_has_return_type(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_docs("rectangle")
|
||||
assert "return_type" in result
|
||||
|
||||
def test_has_param_descriptions(self, loaded_session):
|
||||
result = loaded_session.ifc_shape_docs("polyline")
|
||||
params_with_desc = [p for p in result["params"] if "description" in p]
|
||||
assert len(params_with_desc) > 0
|
||||
|
||||
def test_unknown_method(self, loaded_session):
|
||||
with pytest.raises(ValueError, match="no method"):
|
||||
loaded_session.ifc_shape_docs("nonexistent_method")
|
||||
|
||||
def test_private_method_rejected(self, loaded_session):
|
||||
with pytest.raises(ValueError):
|
||||
loaded_session.ifc_shape_docs("__init__")
|
||||
|
||||
def test_does_not_require_model(self, session):
|
||||
result = session.ifc_shape_docs("circle")
|
||||
assert result["method"] == "circle"
|
||||
|
||||
|
||||
class TestShapeExecute:
|
||||
def test_rectangle(self, loaded_session):
|
||||
result = loaded_session.ifc_shape("rectangle", json.dumps({"size": [4.0, 0.2]}))
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcIndexedPolyCurve"
|
||||
|
||||
def test_circle(self, loaded_session):
|
||||
result = loaded_session.ifc_shape("circle", json.dumps({"center": [0.0, 0.0], "radius": 0.5}))
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcCircle"
|
||||
|
||||
def test_extrude_chained_from_rectangle(self, loaded_session):
|
||||
rect = loaded_session.ifc_shape("rectangle", json.dumps({"size": [4.0, 0.2]}))
|
||||
rect_id = rect["result"]["id"]
|
||||
result = loaded_session.ifc_shape("extrude", json.dumps({"profile_or_curve": rect_id, "magnitude": 3.0}))
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcExtrudedAreaSolid"
|
||||
|
||||
def test_entity_id_as_integer(self, loaded_session):
|
||||
"""Entity IDs should be accepted as plain integers (from JSON)."""
|
||||
rect = loaded_session.ifc_shape("rectangle", json.dumps({"size": [1.0, 1.0]}))
|
||||
rect_id = rect["result"]["id"]
|
||||
# Pass as int, not string
|
||||
result = loaded_session.ifc_shape("extrude", json.dumps({"profile_or_curve": rect_id, "magnitude": 1.0}))
|
||||
assert result["ok"] is True
|
||||
|
||||
def test_rotate_2d_point_returns_list(self, loaded_session):
|
||||
"""Methods returning numpy arrays should give back plain lists."""
|
||||
result = loaded_session.ifc_shape(
|
||||
"rotate_2d_point", json.dumps({"point_2d": [1.0, 0.0], "angle": 90.0, "counter_clockwise": True})
|
||||
)
|
||||
assert result["ok"] is True
|
||||
assert isinstance(result["result"], list)
|
||||
assert len(result["result"]) == 2
|
||||
|
||||
def test_set_polyline_coords_returns_none(self, loaded_session):
|
||||
"""In-place methods that return None should give ok=True, result=None."""
|
||||
rect = loaded_session.ifc_shape("rectangle", json.dumps({"size": [2.0, 2.0]}))
|
||||
rect_id = rect["result"]["id"]
|
||||
result = loaded_session.ifc_shape(
|
||||
"set_polyline_coords",
|
||||
json.dumps({"polyline": rect_id, "coords": [[0.0, 0.0], [3.0, 0.0], [3.0, 3.0], [0.0, 3.0]]}),
|
||||
)
|
||||
assert result["ok"] is True
|
||||
assert result["result"] is None
|
||||
|
||||
def test_unknown_method(self, loaded_session):
|
||||
result = loaded_session.ifc_shape("nonexistent_method", "{}")
|
||||
assert result["ok"] is False
|
||||
assert "error" in result
|
||||
|
||||
def test_private_method_rejected(self, loaded_session):
|
||||
with pytest.raises(IfcSessionError):
|
||||
loaded_session.ifc_shape("__init__", "{}")
|
||||
|
||||
def test_no_model_raises(self, session):
|
||||
with pytest.raises(IfcSessionError, match="No model loaded"):
|
||||
session.ifc_shape("rectangle", "{}")
|
||||
|
||||
def test_params_as_dict(self, loaded_session):
|
||||
"""params can be passed as a dict (not just a JSON string)."""
|
||||
result = loaded_session.ifc_shape("rectangle", {"size": [2.0, 1.0]})
|
||||
assert result["ok"] is True
|
||||
|
||||
def test_error_on_bad_params(self, loaded_session):
|
||||
"""Bad parameters should give ok=False with an error message."""
|
||||
result = loaded_session.ifc_shape("extrude", json.dumps({"profile_or_curve": 999999, "magnitude": 1.0}))
|
||||
assert result["ok"] is False
|
||||
assert "error" in result
|
||||
Reference in New Issue
Block a user