ruff format was never run repo-wide; CI now enforces 'ruff format --check', so normalize every module once. No behavior changes. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.9 KiB
Python
185 lines
5.9 KiB
Python
"""Mocked-COM regression tests for the Sprint-4 COM fixes.
|
|
|
|
These run without a live AutoCAD (and OS-agnostically) by:
|
|
* patching the module-level ``_acad_app`` / ``_acad_doc`` with MagicMocks, and
|
|
* forcing ``_WIN32_AVAILABLE`` True so the win32-guarded paths execute
|
|
(no real win32 calls are made — every win32 attribute is mocked).
|
|
* bypassing the STA executor (replace ``_run`` with a direct caller).
|
|
|
|
Covers: R28 (_find_autocad_hwnd prefers Application.HWND over EnumWindows),
|
|
S3 (block_list populates BlockInfo.description from the definition .Comments).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
import backends.com_backend as cb
|
|
from backends.com_backend import ComBackend
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
def _backend_no_executor() -> ComBackend:
|
|
b = ComBackend()
|
|
|
|
async def _run(func, *args, **kwargs): # bypass the ThreadPoolExecutor
|
|
return func(*args, **kwargs)
|
|
|
|
b._run = _run
|
|
return b
|
|
|
|
|
|
# ── R28 — _find_autocad_hwnd prefers Application.HWND over EnumWindows ───────
|
|
|
|
|
|
async def test_find_hwnd_prefers_application_hwnd(monkeypatch):
|
|
"""The real main-frame handle from the COM app wins; EnumWindows is not
|
|
consulted when the HWND read succeeds."""
|
|
app = MagicMock()
|
|
app.HWND = 0xABCD # known int handle
|
|
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
|
monkeypatch.setattr(cb, "_WIN32_AVAILABLE", True)
|
|
|
|
enum = MagicMock(side_effect=AssertionError("EnumWindows must not be called"))
|
|
monkeypatch.setattr(cb, "win32gui", MagicMock(EnumWindows=enum))
|
|
|
|
assert cb._find_autocad_hwnd() == 0xABCD
|
|
enum.assert_not_called()
|
|
|
|
|
|
async def test_find_hwnd_coerces_hwnd_to_int(monkeypatch):
|
|
"""A COM HWND that arrives as a numeric string is coerced to int."""
|
|
app = MagicMock()
|
|
app.HWND = "43981" # COM can hand back a stringy handle
|
|
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
|
monkeypatch.setattr(cb, "_WIN32_AVAILABLE", True)
|
|
monkeypatch.setattr(cb, "win32gui", MagicMock())
|
|
|
|
result = cb._find_autocad_hwnd()
|
|
assert result == 43981
|
|
assert isinstance(result, int)
|
|
|
|
|
|
async def test_find_hwnd_falls_back_to_enumwindows(monkeypatch):
|
|
"""When the HWND read raises, the EnumWindows title scan is used as a
|
|
fallback and its discovered handle is returned."""
|
|
app = MagicMock()
|
|
type(app).HWND = property( # reading .HWND raises
|
|
lambda self: (_ for _ in ()).throw(RuntimeError("no HWND"))
|
|
)
|
|
monkeypatch.setattr(cb, "_acad_app", lambda: app)
|
|
monkeypatch.setattr(cb, "_WIN32_AVAILABLE", True)
|
|
|
|
def _enum_windows(cb_fn, _extra):
|
|
# Simulate one visible AutoCAD top-level window.
|
|
cb_fn(0x1234, None)
|
|
|
|
win32gui = MagicMock()
|
|
win32gui.EnumWindows.side_effect = _enum_windows
|
|
win32gui.GetWindowText.return_value = "AutoCAD 2025 - [Drawing1.dwg]"
|
|
win32gui.IsWindowVisible.return_value = True
|
|
monkeypatch.setattr(cb, "win32gui", win32gui)
|
|
|
|
assert cb._find_autocad_hwnd() == 0x1234
|
|
win32gui.EnumWindows.assert_called_once()
|
|
|
|
|
|
async def test_find_hwnd_returns_none_when_win32_unavailable(monkeypatch):
|
|
"""Short-circuits to None on non-Windows without touching the COM app."""
|
|
sentinel = MagicMock(side_effect=AssertionError("_acad_app must not be called"))
|
|
monkeypatch.setattr(cb, "_acad_app", sentinel)
|
|
monkeypatch.setattr(cb, "_WIN32_AVAILABLE", False)
|
|
|
|
assert cb._find_autocad_hwnd() is None
|
|
sentinel.assert_not_called()
|
|
|
|
|
|
# ── S3 — block_list populates BlockInfo.description from .Comments ───────────
|
|
|
|
|
|
class _Block:
|
|
"""Minimal stand-in for an AutoCAD block definition."""
|
|
|
|
def __init__(self, name, comments="", count=0, is_xref=False, raise_comments=False):
|
|
self.Name = name
|
|
self._comments = comments
|
|
self.Count = count
|
|
self.IsXRef = is_xref
|
|
self.Origin = (0.0, 0.0, 0.0)
|
|
self._raise_comments = raise_comments
|
|
|
|
@property
|
|
def Comments(self):
|
|
if self._raise_comments:
|
|
raise RuntimeError("Comments not supported")
|
|
return self._comments
|
|
|
|
def Item(self, _j): # no attribute definitions in these fixtures
|
|
return MagicMock(ObjectName="AcDbEntity")
|
|
|
|
|
|
class _Blocks:
|
|
def __init__(self, items):
|
|
self._items = items
|
|
self.Count = len(items)
|
|
|
|
def Item(self, i):
|
|
return self._items[i]
|
|
|
|
|
|
def _doc_with_blocks(items):
|
|
doc = MagicMock()
|
|
doc.Blocks = _Blocks(items)
|
|
return doc
|
|
|
|
|
|
async def test_block_list_populates_description_from_comments(monkeypatch):
|
|
doc = _doc_with_blocks([_Block("BOLT", comments="M8 hex bolt")])
|
|
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
|
|
|
b = _backend_no_executor()
|
|
blocks = await b.block_list()
|
|
|
|
assert len(blocks) == 1
|
|
assert blocks[0].name == "BOLT"
|
|
assert blocks[0].description == "M8 hex bolt"
|
|
|
|
|
|
async def test_block_list_description_defaults_empty_when_comments_raise(monkeypatch):
|
|
doc = _doc_with_blocks([_Block("NUT", raise_comments=True)])
|
|
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
|
|
|
b = _backend_no_executor()
|
|
blocks = await b.block_list()
|
|
|
|
assert blocks[0].description == "" # try/except default, not a crash
|
|
|
|
|
|
async def test_block_list_description_empty_when_comments_none(monkeypatch):
|
|
doc = _doc_with_blocks([_Block("WASHER", comments=None)])
|
|
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
|
|
|
b = _backend_no_executor()
|
|
blocks = await b.block_list()
|
|
|
|
assert blocks[0].description == "" # `Comments or ""` normalises None
|
|
|
|
|
|
async def test_block_list_skips_anonymous_layout_blocks(monkeypatch):
|
|
doc = _doc_with_blocks(
|
|
[
|
|
_Block("*Model_Space", comments="ignored"),
|
|
_Block("GEAR", comments="spur gear"),
|
|
]
|
|
)
|
|
monkeypatch.setattr(cb, "_acad_doc", lambda: doc)
|
|
|
|
b = _backend_no_executor()
|
|
blocks = await b.block_list()
|
|
|
|
assert [blk.name for blk in blocks] == ["GEAR"]
|
|
assert blocks[0].description == "spur gear"
|