enter_layout read the caller's space from `backend._current_space`, which only
EzdxfBackend has. On ComBackend the getattr fell through to "Model", so
`sheet_frame` / `titleblock_apply` with a `layout=` dropped a live caller onto
the Model tab instead of putting them back -- the opposite of what the
docstring and both tool docs promise, and a dual-engine parity break in new
shared infrastructure. Measured on the live seat (AutoCAD 2026, scratch
document from drawing_new(), closed with discard):
scratch document: Drawing24.dwg
layouts: ['Layout1', 'Layout2', 'Model']
B) current BEFORE: Layout2
B) the value the OLD code read (backend._current_space):
<<absent -> getattr default 'Model'>>
`layout_list()` already reported the right value two lines earlier on both
engines (ezdxf from $TILEMODE / the active layout, COM from
`doc.ActiveLayout.Name` -- read live as "Layout2" above), so `previous` now
comes from `listing["current"]`.
The projection-angle symbol was justified by an in-code derivation that cannot
decide the question, and the test restated the implementation. The derivation
was also applied inconsistently: the third-angle symbol mirrored the cone as
well as the end view, leaving the trapezoid's short side pointing AWAY from the
circles -- which is the first-angle arrangement. Transcribed instead from two
independent reproductions of the ISO 5456-2 figure, both downloaded and
measured (ISO's own figure is paywalled and was NOT read; the module says so):
* FreeCAD's ISO 5457 sheet template A3_Landscape_ISO5457_advanced.svg,
`first_angle_trapezoid` = "m 389,222 -10,2.5 v 5 l 10,2.5 z" -> 5-unit side
at x=379, 10-unit side at x=389; circles r2.5/r5 at cx=396.
* Wikimedia Commons "Convention placement vues dessin technique.svg", both
symbols side by side, labelled FR and US: FR 35.856 @ x=13.343 / 66.135 @
x=81.869, circles x=148.005; US 35.856 @ x=380.275 / 66.135 @ x=448.801,
circles x=314.140.
Both draw the trapezoid the same way -- short side left -- and move only the
end view: first angle puts it right of the cone, third angle left. Equivalently
the short side points away from the circles in first angle and towards them in
third, which is how every source states it in words. `third` now follows.
Tests: the reference figures are transcribed into the test as data, and the
symbol is asserted to land its three landmarks in the order each figure does,
so the test cannot pass by agreeing with the code. The layout restore is pinned
through a proxy that hides `_current_space` from the real backend rather than a
fake that owns members ComBackend does not have. Both new tests fail on the
previous code (measured: "assert 16.0 < 8.0" for the mirrored cone; "- SheetB
+ Model" for the tab).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
"""Shared fixtures for AutoCAD MCP tests."""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from backends.ezdxf_backend import EzdxfBackend
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def pin_headless_backend(request, monkeypatch):
|
|
"""Every test talks to the headless engine unless it says otherwise.
|
|
|
|
This suite runs on developer machines that have AutoCAD installed, and a
|
|
test that opens ``Client(server.mcp)`` without pinning would run the real
|
|
lifespan: ``AUTOCAD_MCP_BACKEND`` unset means ``auto``, which on Windows
|
|
means attaching to the operator's open, unsaved drawing and creating
|
|
entities in it. That has actually happened here. Defaulting the whole suite
|
|
to ``ezdxf`` is the only guard that covers tests nobody has written yet.
|
|
|
|
Opt out with ``@pytest.mark.live_backend`` for a test that deliberately
|
|
exercises backend selection.
|
|
"""
|
|
if request.node.get_closest_marker("live_backend"):
|
|
return
|
|
monkeypatch.setenv("AUTOCAD_MCP_BACKEND", "ezdxf")
|
|
|
|
|
|
def pytest_configure(config):
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"live_backend: test drives backend selection itself; do not pin AUTOCAD_MCP_BACKEND",
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def backend():
|
|
"""Create an ezdxf backend with a fresh empty document."""
|
|
b = EzdxfBackend()
|
|
await b.connect()
|
|
await b.drawing_new()
|
|
yield b
|
|
await b.disconnect()
|
|
|
|
|
|
class WithoutPrivateState:
|
|
"""The real backend with its ezdxf-only private attributes taken away.
|
|
|
|
``EzdxfBackend`` tracks the current tab in ``self._current_space``;
|
|
``ComBackend`` does not -- the live tab lives in AutoCAD
|
|
(``doc.ActiveLayout``), so there is nothing of that name on it. Shared code
|
|
that reaches for the private attribute therefore silently takes the
|
|
``getattr`` default on every live seat, and no ezdxf-backed test can see
|
|
it. Wrapping the real backend (rather than writing a fake that owns members
|
|
the real object never had) keeps every other call honest.
|
|
"""
|
|
|
|
def __init__(self, inner, hide=("_current_space",)):
|
|
object.__setattr__(self, "_inner", inner)
|
|
object.__setattr__(self, "_hidden", frozenset(hide))
|
|
|
|
def __getattr__(self, name):
|
|
if name in self._hidden:
|
|
raise AttributeError(f"{type(self._inner).__name__} has no attribute {name!r}")
|
|
return getattr(self._inner, name)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def backend_without_private_state(backend):
|
|
"""`backend`, minus the private attributes only the headless engine has."""
|
|
return WithoutPrivateState(backend)
|