fix(com): four live defects the settings smoke measured on AutoCAD 2026 — rejected calls after a document switch, typelib spelling of color/Lineweight, narrowed layer-state dictionary, layer 0 Freeze put
Measured 2026-09-22 by scripts/smoke_settings_com.py --build-dwt against the running AutoCAD 2026 (25.1s), each on a scratch Documents.Add() document: 1. RPC_E_CALL_REJECTED (-2147418111) on Documents.Count straight after Documents.Open, after Close(False) inside document_close, and in _ensure_document_state: AutoCAD's own message filter refuses incoming calls while it is still switching documents. pywin32 exposes no CoRegisterMessageFilter, so _wait_out_rejected_call waits the window out for read-only callables (0.25 s -> 1 s pauses, 20 s budget, inside COM_CALL_TIMEOUT) at those sites and after Documents.Add/Open; any other com_error is not retried. 2. layer_list raised "'IAcadLayer' object has no attribute 'Color'. Did you mean: 'color'?": acax25enu.tlb spells IAcadEntity/IAcadLayer `color` and `Lineweight`. Late-bound dispatch resolved .Color/.LineWeight case-insensitively; with a makepy cache present (this machine's gen_py was regenerated on 2026-09-17, and every object a method returns is then a generated wrapper) the lookup is exact. All 17 sites now use the typelib spelling, which works on both wrappers; the fakes model the measured names. 3. layer_state_restore raised "'IAcadObject' object has no attribute 'Count'": Dictionaries.Item and IAcadDictionary.Item are declared IAcadObject* (the narrowing _com_unnarrow already handles for MLEADER styles). The save path went through the typed Dictionaries.Add, so only the reopen paths failed. Both now un-narrow; the fakes narrow like pywin32. 4. The Freeze put on layer 0 raises 'Invalid layer' whatever the value, even False while thawed (color/Linetype/Lineweight/Plottable/LayerOn/Lock/ ActiveLayer all fine on it), so a state saved with 0 current restored with two warnings and 0 unapplied. Freeze is written only when it changes the layer. 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
0514f8f15d
commit
b54271f4ac
@@ -815,3 +815,95 @@ async def test_com_drawing_close_refuses_the_untitled_save_dialog(com_backend):
|
||||
with pytest.raises(ValueError, match="Save dialog"):
|
||||
await backend.drawing_close(save=True)
|
||||
assert app.Documents._docs[2].calls == []
|
||||
|
||||
|
||||
# ── the busy window after a document switch ──────────────────────────────────
|
||||
|
||||
|
||||
class _BusyDocuments(_FakeDocuments):
|
||||
"""``Documents`` that answers ``RPC_E_CALL_REJECTED`` a fixed number of
|
||||
times before ``Count`` works — AutoCAD's own message filter refusing the
|
||||
incoming call while it is still switching documents (measured 2026-09-22
|
||||
by ``scripts/smoke_settings_com.py --build-dwt`` on AutoCAD 2026, twice:
|
||||
``Documents.Count`` right after ``Documents.Open`` and after ``Close``)."""
|
||||
|
||||
def __init__(self, rejections: int):
|
||||
super().__init__()
|
||||
self.rejections_left = rejections
|
||||
self.rejected = 0
|
||||
|
||||
@property
|
||||
def Count(self):
|
||||
if self.rejections_left > 0:
|
||||
self.rejections_left -= 1
|
||||
self.rejected += 1
|
||||
pywintypes = pytest.importorskip("pywintypes")
|
||||
raise pywintypes.com_error(-2147418111, "Call was rejected by callee.", None, None)
|
||||
return super().Count
|
||||
|
||||
|
||||
async def test_com_ensure_document_state_waits_out_a_rejected_call(com_backend, monkeypatch):
|
||||
backend, app = com_backend
|
||||
busy = _BusyDocuments(rejections=3)
|
||||
busy._docs.extend(app.Documents._docs)
|
||||
app.Documents = busy
|
||||
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||
await backend._ensure_document_state()
|
||||
assert busy.rejected == 3
|
||||
assert backend._document_scope_key == ("First.dwg", "C:/work/First.dwg")
|
||||
|
||||
|
||||
async def test_com_ensure_document_state_gives_up_after_its_budget(com_backend, monkeypatch):
|
||||
"""A seat that keeps rejecting (an operator mid-command) still surfaces
|
||||
the error — the retry is a window, not a wait forever."""
|
||||
backend, app = com_backend
|
||||
busy = _BusyDocuments(rejections=10_000)
|
||||
busy._docs.extend(app.Documents._docs)
|
||||
app.Documents = busy
|
||||
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||
monkeypatch.setattr(backend, "_REJECTED_RETRY_BUDGET_S", 0.02)
|
||||
pywintypes = pytest.importorskip("pywintypes")
|
||||
with pytest.raises(pywintypes.com_error) as excinfo:
|
||||
await backend._ensure_document_state()
|
||||
assert excinfo.value.args[0] == -2147418111
|
||||
assert 1 < busy.rejected < 100, "bounded by the budget, not by the rejection count"
|
||||
|
||||
|
||||
async def test_com_ensure_document_state_does_not_retry_other_errors(com_backend):
|
||||
backend, app = com_backend
|
||||
pywintypes = pytest.importorskip("pywintypes")
|
||||
|
||||
class _Broken(_FakeDocuments):
|
||||
calls = 0
|
||||
|
||||
@property
|
||||
def Count(self):
|
||||
_Broken.calls += 1
|
||||
raise pywintypes.com_error(-2147417848, "disconnected", None, None)
|
||||
|
||||
app.Documents = _Broken()
|
||||
with pytest.raises(pywintypes.com_error):
|
||||
await backend._ensure_document_state()
|
||||
assert _Broken.calls == 1
|
||||
|
||||
|
||||
async def test_com_document_close_waits_out_the_rejection_after_close(com_backend, monkeypatch):
|
||||
"""The third measured site: ``Documents.Count`` straight after
|
||||
``doc.Close(False)`` inside ``document_close`` itself, before the
|
||||
``_ensure_document_state`` read is even reached."""
|
||||
backend, app = com_backend
|
||||
busy = _BusyDocuments(rejections=0)
|
||||
busy._docs.extend(app.Documents._docs)
|
||||
app.Documents = busy
|
||||
monkeypatch.setattr(backend, "_REJECTED_RETRY_FIRST_PAUSE_S", 0.001)
|
||||
untitled = busy._docs[2]
|
||||
original_close = untitled.Close
|
||||
|
||||
def _close_then_go_busy(save_changes):
|
||||
original_close(save_changes)
|
||||
busy.rejections_left = 2
|
||||
|
||||
untitled.Close = _close_then_go_busy
|
||||
result = await backend.document_close("Drawing1.dwg", discard=True)
|
||||
assert busy.rejected == 2
|
||||
assert result["open_documents"] == 2 and result["active"] == "Second.dwg"
|
||||
|
||||
Reference in New Issue
Block a user