Files
autocad-mcp-pro/tests/test_table_leader.py
T
Umut CANandClaude Opus 5.5 9168d61397 feat(task-6): door, window and room schedules read from the drawing
engineering/arch/schedule.py turns the plan records into schedule rows: doors
by tag with width, height, swing, hand and wall; windows by tag with width,
height, sill and wall; rooms by number with name and the measured area the
label carries, plus a total. Tags compare as numbers (D2 before D10). An
opening without a tag gets the next free D1 / W1 - K1 / P1 in Turkish - in
wall order; an explicit tag is never renumbered and a generated one never
repeats it. A value the model does not carry is an empty cell, never a default,
and Turkish output writes decimal commas.

draw_schedule reads the ACADMCP_ARCH records off the drawing through the one
reader rooms.py already has and draws a real TABLE the way the ISO 7573 parts
list does - top-left insertion, the table growing down, the parts list's 7 mm
rows and 2.5 mm text at the plot scale, the engine's representation reported.
Nothing to schedule, an unknown kind or language and more rows than one TABLE
takes are refused before anything is drawn.

arch_schedule joins arch_room and arch_rooms_detect under SECTION 25's rooms
marker (banner 3 tools), shares the TABLE command with entity_create_table and
bom_table, and the counts follow: 232 tools in 25 groups.

Live fix in com_backend.entity_create_table. MEASURED on AutoCAD 2026: IAcadTable
has no TextHeight property. pywin32 raised AttributeError on
`table.TextHeight = h`, the old code swallowed it, and every live table kept
the Standard style's heights (title 6.0, header and data 4.5), which is 0.1 mm
on paper at 1:50. The member that works is SetCellTextHeight(row, col, h),
probed on a scratch document: every cell read back 125.0 via
GetCellTextHeight. The row heights stayed 350.0 and GetBoundingBox matched the
reported bbox (10000,3250)-(15750,5000). Then draw_schedule ran end to end
through ComBackend on a scratch document: a native TABLE on A-SYMB-T-N for
doors (5 rows) and rooms/tr (6 rows, total 45,00 m²), and the document was
closed afterwards. The new fake in tests/test_table_leader.py has only the
measured members.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:56:05 +03:00

212 lines
7.2 KiB
Python

from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from backends.base import EntityInfo
from backends.com_backend import ComBackend
pytestmark = pytest.mark.asyncio
async def test_ezdxf_table_returns_composite_contract(backend):
result = await backend.entity_create_table(
10,
100,
rows=[["P-01", "4"], ["P-02", "2"]],
headers=["Part", "Qty"],
title="BOM",
layer="TEXT",
)
assert result.type == "TABLE"
assert result.layer == "TEXT"
assert result.properties["representation"] == "composite"
assert result.properties["logical_group_id"].startswith("table:")
assert len(result.properties["child_handles"]) >= 10
assert result.properties["bounds"] == {"min": [10.0, 72.0], "max": [30.0, 100.0]}
async def test_ezdxf_table_survives_save_and_reopen(backend, tmp_path):
result = await backend.entity_create_table(0, 30, rows=[["A", "1"]], headers=["Name", "Qty"])
path = tmp_path / "table.dxf"
await backend.drawing_save_as(str(path), fmt="dxf")
await backend.drawing_open(str(path))
entities = await backend.entity_list(limit=100)
handles = {entity.handle for entity in entities}
texts = {
entity.properties.get("text") for entity in entities if entity.type in {"TEXT", "MTEXT"}
}
assert set(result.properties["child_handles"]).issubset(handles)
assert {"Name", "Qty", "A", "1"}.issubset(texts)
async def test_table_rejects_ragged_rows_and_size_limit(backend):
with pytest.raises(RuntimeError, match="same number of columns"):
await backend.entity_create_table(0, 0, rows=[["A", "B"], ["C"]])
with pytest.raises(RuntimeError, match="200 rows"):
await backend.entity_create_table(0, 0, rows=[["A"]] * 201)
async def test_ezdxf_mleader_returns_composite_contract(backend):
result = await backend.leader_create_mleader(
[[0, 0], [10, 5], [30, 5]],
"SURFACE A",
layer="DIM",
)
assert result.type == "MLEADER"
assert result.properties["representation"] == "composite"
assert result.properties["logical_group_id"].startswith("mleader:")
assert result.properties["text"] == "SURFACE A"
assert len(result.properties["child_handles"]) == 3
async def test_mleader_requires_two_points(backend):
with pytest.raises(RuntimeError, match="at least two points"):
await backend.leader_create_mleader([[0, 0]], "note")
async def test_com_table_uses_native_add_table():
backend = ComBackend()
async def run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
backend._run = run_inline
mspace = MagicMock()
table = MagicMock()
table.Handle = "A1"
mspace.AddTable.return_value = table
info = EntityInfo("A1", "ACAD_TABLE", "TEXT", 256, "ByLayer", True, {})
with (
patch("backends.com_backend._msp", return_value=mspace),
patch("backends.com_backend._apoint", side_effect=lambda *p: tuple(p)),
patch("backends.com_backend._entity_info", return_value=info),
patch("backends.com_backend._apply_entity_attrs"),
patch("backends.com_backend._regen"),
):
result = await backend.entity_create_table(
0, 50, [["A", "1"]], headers=["Name", "Qty"], title="BOM"
)
mspace.AddTable.assert_called_once()
assert mspace.AddTable.call_args[0][1:3] == (3, 2)
assert table.SetText.call_count == 6
assert result.type == "TABLE"
assert result.properties["representation"] == "native"
async def test_com_mleader_uses_native_add_mleader():
backend = ComBackend()
async def run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
backend._run = run_inline
mspace = MagicMock()
leader = MagicMock()
leader.Handle = "B1"
mspace.AddMLeader.return_value = leader
info = EntityInfo("B1", "ACAD_MLEADER", "DIM", 256, "ByLayer", True, {})
with (
patch("backends.com_backend._msp", return_value=mspace),
patch("backends.com_backend._av", side_effect=lambda values: values),
patch("backends.com_backend._entity_info", return_value=info),
patch("backends.com_backend._apply_entity_attrs"),
patch("backends.com_backend._regen"),
):
result = await backend.leader_create_mleader([[0, 0], [20, 5]], "NOTE")
mspace.AddMLeader.assert_called_once_with([0.0, 0.0, 0.0, 20.0, 5.0, 0.0], 0)
assert leader.TextString == "NOTE"
assert result.type == "MLEADER"
assert result.properties["representation"] == "native"
async def test_server_registers_table_and_mleader_tools():
import server
names = {tool.name for tool in await server._registered_tools()}
assert "entity_create_table" in names
assert "leader_create_mleader" in names
class _MeasuredTable:
"""The IAcadTable members a live AutoCAD 2026 seat exposes (measured).
MEASURED on AutoCAD 2026 (track F task 6): the table has no ``TextHeight``
property - pywin32 answers ``table.TextHeight = h`` with AttributeError -
and a table left alone keeps the Standard style's heights (title 6.0, header
and data 4.5). ``SetCellTextHeight(row, col, h)`` is the member that sets it.
A fake that accepted ``TextHeight`` would hide exactly that defect.
"""
def __init__(self, rows, columns):
self.Handle = "A1"
self.Rows, self.Columns = rows, columns
self._heights = {
(r, c): (6.0 if r == 0 else 4.5) for r in range(rows) for c in range(columns)
}
def __setattr__(self, name, value):
if name == "TextHeight":
raise AttributeError(f"object has no attribute {name!r}")
object.__setattr__(self, name, value)
def SetColumnWidth(self, column, width):
pass
def SetRowHeight(self, row, height):
pass
def SetText(self, row, column, value):
pass
def SetCellTextHeight(self, row, column, height):
self._heights[(row, column)] = float(height)
def GetCellTextHeight(self, row, column):
return self._heights[(row, column)]
async def test_com_table_sets_every_cell_text_height_through_the_measured_member():
backend = ComBackend()
async def run_inline(func, *args, **kwargs):
return func(*args, **kwargs)
backend._run = run_inline
mspace = MagicMock()
made: list[_MeasuredTable] = []
def add_table(_point, rows, columns, _row_height, _column_width):
made.append(_MeasuredTable(rows, columns))
return made[-1]
mspace.AddTable.side_effect = add_table
info = EntityInfo("A1", "ACAD_TABLE", "TEXT", 256, "ByLayer", True, {})
with (
patch("backends.com_backend._msp", return_value=mspace),
patch("backends.com_backend._apoint", side_effect=lambda *p: tuple(p)),
patch("backends.com_backend._entity_info", return_value=info),
patch("backends.com_backend._apply_entity_attrs"),
patch("backends.com_backend._regen"),
):
await backend.entity_create_table(
0, 50, [["A", "1"]], headers=["Name", "Qty"], title="BOM", text_height=125.0
)
(table,) = made
assert {
(row, column): table.GetCellTextHeight(row, column)
for row in range(table.Rows)
for column in range(table.Columns)
} == {(row, column): 125.0 for row in range(3) for column in range(2)}