fix(com): every system variable is read and written on the ActiveX document, not the Application
AcadApplication has no GetVariable/SetVariable members; they live on
AcadDocument. Measured on the running AutoCAD 2026 (25.1s, 2026-09-22,
scratch Documents.Add(), Close(False)):
hasattr(app, 'GetVariable') -> False
hasattr(app, 'SetVariable') -> False
app.GetVariable('INSUNITS') -> AttributeError: ... IAcadApplication
instance ... has no attribute 'GetVariable'
doc.GetVariable('INSUNITS') -> 4
doc.GetVariable('MEASUREMENT') -> 1
doc.GetVariable('FILEDIA') -> 1; doc.SetVariable('FILEDIA', 0) -> None,
reads back 0, restored to 1
doc.GetVariable('AUDITCTL') -> 0, 'CMDACTIVE' -> 0, 'USERS1' -> ''
Fifteen call sites in backends/com_backend.py still went through the
Application inside `try/except ... log.debug`, so on a live seat they
silently degraded: MEASUREMENT defaulted every seat to metric,
drawing_info reported units from a failed INSUNITS read, drawing_audit
never set AUDITCTL, the CMDACTIVE guards of system_run_command /
system_run_lisp never fired and run_lisp never read USERS1 back. All are
rerouted through `_acad_doc()`; the try/except stays only where an
optional read is defaulted (INSUNITS -> "Unknown", AUDITCTL absent on a
host that lacks it, USERS1 after the expression ran) and the log line
says so.
Linetype loading, measured the same way: `SendCommand("_-LINETYPE _LOAD
CENTER acadiso.lin\n\n")` loads nothing with FILEDIA=0 and leaves the
command at `Enter an option [?/Create/Load/Set]:` (LASTPROMPT), which
makes the whole seat answer RPC_E_CALL_REJECTED to every client until
ESC is pressed; with FILEDIA=1 it opens the modal "Select Linetype File"
dialog. `doc.Linetypes.Load("CENTER", "acadiso.lin")` returns None and
the table grows 3 -> 4; a repeat raises 'Duplicate record name', an
unknown name 'Undefined linetype', a missing file 'File system error'.
`_ensure_linetype_loaded` and `linetype_load` now call `Linetypes.Load`
(MEASUREMENT still read on the document to pick the .lin), touch no
FILEDIA, and report a failure with the English excepinfo description.
`apply_standard` reads every variable it will write before the first
style is created, so a read the engine refuses is a refusal with nothing
written (it used to fail after ISO-25 / ISOCP were created and current).
Fakes: every application fake refuses GetVariable/SetVariable
(tests/test_styles.py's COM fixture carried them on the Application,
enshrining the wrong shape); the members exist on document fakes only.
Regression tests per rerouted site: linetype load via Linetypes.Load with
MEASUREMENT on the document, drawing_info units from the document,
AUDITCTL set/restored on the document, CMDACTIVE/USERS1 on the document,
apply_standard refusing before any write.
Live end-to-end with the real ComBackend on a scratch document:
apply_standard('iso') -> INSUNITS 4, ISO-25 (already in acadiso.dwt,
created: false) and ISOCP (created: true) current, second call created:
false / changed: {}; linetype_load('CENTER') -> Linetypes 3 -> 4, FILEDIA
stays 1, repeat already_loaded, 'NOSUCHLT' refused by name;
_ensure_linetype_loaded('HIDDEN') -> 3 -> 4; drawing_info units 'mm';
drawing_audit AUDITCTL 0 -> 0; system_run_command ok;
system_run_lisp('(+ 40 2)') -> '42'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
70afd57831
commit
f7528b2fa5
+44
-15
@@ -73,31 +73,60 @@ async def test_set_variable_coerces_string_to_sysvar_int_type(monkeypatch):
|
||||
# ── R23 / N6 — run_lisp guard + value capture ───────────────────────────────
|
||||
|
||||
|
||||
class _App:
|
||||
"""The ActiveX Application has no sysvar members (measured, AutoCAD 2026:
|
||||
``hasattr(app, "GetVariable") is False``); CMDACTIVE and USERS1 are read
|
||||
on the document, so this fake refuses the old call shape outright."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise AttributeError(f"AutoCAD.Application.{name}")
|
||||
|
||||
|
||||
async def test_run_lisp_refuses_when_cmdactive(monkeypatch):
|
||||
app = MagicMock()
|
||||
app.GetVariable.side_effect = lambda n: 1 if n == "CMDACTIVE" else 0
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: _Doc())
|
||||
|
||||
b = _backend_no_executor()
|
||||
with pytest.raises(RuntimeError):
|
||||
await b.system_run_lisp("(+ 1 2)")
|
||||
|
||||
|
||||
async def test_run_lisp_captures_value_from_users1(monkeypatch):
|
||||
app = MagicMock()
|
||||
app.GetVariable.side_effect = lambda n: {"CMDACTIVE": 0, "USERS1": "3"}.get(n, 0)
|
||||
doc = _Doc()
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
||||
doc.SetVariable("CMDACTIVE", 1)
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: _App())
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
||||
|
||||
b = _backend_no_executor()
|
||||
with pytest.raises(RuntimeError, match="CMDACTIVE"):
|
||||
await b.system_run_lisp("(+ 1 2)")
|
||||
assert doc.sent == [], "nothing reached AutoCAD"
|
||||
|
||||
|
||||
async def test_run_lisp_captures_value_from_users1(monkeypatch):
|
||||
doc = _Doc()
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: _App())
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
||||
|
||||
def _send(cmd):
|
||||
doc.sent.append(cmd)
|
||||
if "USERS1" in cmd:
|
||||
doc._vars["USERS1"] = "3" # what (setvar "USERS1" ...) leaves behind
|
||||
|
||||
doc.SendCommand = _send
|
||||
b = _backend_no_executor()
|
||||
out = await b.system_run_lisp("(+ 1 2)")
|
||||
|
||||
assert out["result"] == "3" # captured, not the bogus "nil"
|
||||
assert out["result"] == "3" # captured from the document, not the bogus "nil"
|
||||
assert any("USERS1" in c for c in doc.sent) # value stashed out-of-band
|
||||
|
||||
|
||||
async def test_run_command_reads_cmdactive_on_the_document(monkeypatch):
|
||||
doc = _Doc()
|
||||
monkeypatch.setattr(cb, "_acad_app", lambda: _App())
|
||||
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
||||
|
||||
b = _backend_no_executor()
|
||||
out = await b.system_run_command("_ZOOM E")
|
||||
assert out == {"ok": True, "command": "_ZOOM E"} and doc.sent == ["_ZOOM E\n"]
|
||||
|
||||
doc.SetVariable("CMDACTIVE", 1)
|
||||
with pytest.raises(RuntimeError, match="CMDACTIVE=1"):
|
||||
await b.system_run_command("_ZOOM E")
|
||||
assert doc.sent == ["_ZOOM E\n"], "a busy seat gets nothing new"
|
||||
|
||||
|
||||
# ── R16 — transaction flag cleared even on error ────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user