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
559 lines
21 KiB
Python
559 lines
21 KiB
Python
"""Portable layer states: save → change → restore equals the snapshot.
|
|
|
|
These are the server's own layer states — JSON chunks in an XRECORD under
|
|
``ACADMCP_LAYERSTATES`` in the drawing's named object dictionary. They live
|
|
in the file and travel with it; they do NOT appear in AutoCAD's Layer States
|
|
Manager. The codec is engine-neutral, so the ezdxf tests prove the format and
|
|
the COM tests prove the ActiveX call shapes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from backends.base import LayerInfo
|
|
from engineering.environment.layer_states import (
|
|
DICT_NAME,
|
|
PROPERTIES,
|
|
decode_state,
|
|
diff_snapshot,
|
|
encode_state,
|
|
snapshot_from_layers,
|
|
validate_properties,
|
|
)
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
def _layer(name, **overrides) -> LayerInfo:
|
|
base = dict(
|
|
name=name,
|
|
color=7,
|
|
linetype="Continuous",
|
|
lineweight=-3,
|
|
is_on=True,
|
|
is_frozen=False,
|
|
is_locked=False,
|
|
is_current=False,
|
|
)
|
|
base.update(overrides)
|
|
return LayerInfo(**base)
|
|
|
|
|
|
# ── codec ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_snapshot_records_every_property_and_the_current_layer():
|
|
state = snapshot_from_layers(
|
|
[_layer("0"), _layer("HIDDEN", color=8, linetype="HIDDEN", lineweight=35, is_frozen=True)],
|
|
"HIDDEN",
|
|
plot={"HIDDEN": False},
|
|
description="before plotting",
|
|
)
|
|
assert state == {
|
|
"version": 1,
|
|
"description": "before plotting",
|
|
"current_layer": "HIDDEN",
|
|
"layers": {
|
|
"0": {
|
|
"on": True,
|
|
"frozen": False,
|
|
"locked": False,
|
|
"color": 7,
|
|
"linetype": "Continuous",
|
|
"lineweight": -3,
|
|
"plot": True,
|
|
},
|
|
"HIDDEN": {
|
|
"on": True,
|
|
"frozen": True,
|
|
"locked": False,
|
|
"color": 8,
|
|
"linetype": "HIDDEN",
|
|
"lineweight": 35,
|
|
"plot": False,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def test_encode_chunks_are_at_most_255_ascii_characters_and_round_trip():
|
|
layers = [_layer(f"LAYER-{i:03d}-{'x' * 40}") for i in range(60)]
|
|
state = snapshot_from_layers(layers, "0")
|
|
chunks = encode_state(state)
|
|
assert len(chunks) > 5
|
|
assert all(len(c) <= 255 and c.isascii() for c in chunks)
|
|
assert decode_state(chunks) == state
|
|
|
|
|
|
def test_unicode_layer_names_survive_the_ascii_chunking():
|
|
state = snapshot_from_layers([_layer("KÖRPER-Ø20")], "KÖRPER-Ø20")
|
|
chunks = encode_state(state)
|
|
assert all(c.isascii() for c in chunks), "escaped so a chunk boundary never splits a code point"
|
|
assert decode_state(chunks)["current_layer"] == "KÖRPER-Ø20"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"chunks, fragment",
|
|
[
|
|
(["not json"], "not valid JSON"),
|
|
([json.dumps({"version": 2, "layers": {}})], "version"),
|
|
([json.dumps({"version": 1, "layers": {"A": {"on": True}}})], "lacks"),
|
|
([json.dumps([1, 2])], "version"),
|
|
],
|
|
)
|
|
def test_decode_refuses_anything_that_is_not_a_state(chunks, fragment):
|
|
with pytest.raises(ValueError, match=fragment):
|
|
decode_state(chunks)
|
|
|
|
|
|
def test_diff_reports_missing_and_new_layers_sorted():
|
|
state = snapshot_from_layers([_layer("A"), _layer("B"), _layer("C")], "0")
|
|
missing, new = diff_snapshot(state, [_layer("C"), _layer("Z"), _layer("A"), _layer("Y")])
|
|
assert missing == ["B"] and new == ["Y", "Z"]
|
|
|
|
|
|
def test_validate_properties_defaults_to_all_and_names_the_bad_one():
|
|
assert validate_properties(None) == PROPERTIES
|
|
assert validate_properties(["color", "on", "color"]) == ("color", "on")
|
|
with pytest.raises(ValueError, match=r"properties\[1\]: 'colour'"):
|
|
validate_properties(["on", "colour"])
|
|
with pytest.raises(TypeError, match="list"):
|
|
validate_properties("on")
|
|
with pytest.raises(ValueError, match="at least one"):
|
|
validate_properties([])
|
|
|
|
|
|
# ── headless engine ─────────────────────────────────────────────────────────
|
|
|
|
|
|
async def _seed(backend):
|
|
await backend.layer_create("GEOMETRY", color=7, linetype="Continuous", lineweight=50)
|
|
await backend.layer_create("HIDDEN", color=8, linetype="HIDDEN", lineweight=35)
|
|
await backend.layer_create("CENTER", color=1, linetype="CENTER", lineweight=25)
|
|
await backend.layer_set_current("GEOMETRY")
|
|
await backend.layer_freeze("CENTER")
|
|
await backend.layer_hide("HIDDEN")
|
|
|
|
|
|
async def _table(backend) -> dict:
|
|
return {
|
|
lyr.name: (
|
|
lyr.color,
|
|
lyr.linetype,
|
|
lyr.lineweight,
|
|
lyr.is_on,
|
|
lyr.is_frozen,
|
|
lyr.is_locked,
|
|
lyr.is_current,
|
|
)
|
|
for lyr in await backend.layer_list()
|
|
}
|
|
|
|
|
|
async def test_save_change_restore_equals_the_snapshot(backend):
|
|
await _seed(backend)
|
|
before = await _table(backend)
|
|
saved = await backend.layer_state_save("PLOT", description="plot set")
|
|
# measured: ezdxf.new() already has "0" and "Defpoints", so the seeded
|
|
# table is five layers and serialises to three <= 255-character chunks
|
|
assert saved == {
|
|
"ok": True,
|
|
"name": "PLOT",
|
|
"layer_count": 5,
|
|
"replaced": False,
|
|
"chunks": 3,
|
|
"backend": "ezdxf",
|
|
}
|
|
|
|
await backend.layer_thaw("CENTER")
|
|
await backend.layer_show("HIDDEN")
|
|
await backend.layer_lock("GEOMETRY")
|
|
await backend.layer_modify("HIDDEN", color=3, linetype="Continuous", lineweight=13)
|
|
await backend.layer_set_current("0")
|
|
assert await _table(backend) != before
|
|
|
|
restored = await backend.layer_state_restore("PLOT")
|
|
assert restored["applied"] == {
|
|
"layers": 5,
|
|
"properties": list(PROPERTIES),
|
|
"current_layer": "GEOMETRY",
|
|
}
|
|
assert restored["missing_layers"] == [] and restored["new_layers"] == []
|
|
assert await _table(backend) == before
|
|
|
|
|
|
async def test_restore_applies_only_the_requested_properties(backend):
|
|
await _seed(backend)
|
|
await backend.layer_state_save("S")
|
|
await backend.layer_thaw("CENTER")
|
|
await backend.layer_modify("CENTER", color=5)
|
|
result = await backend.layer_state_restore("S", properties=["color"])
|
|
assert result["applied"]["properties"] == ["color"]
|
|
center = next(lyr for lyr in await backend.layer_list() if lyr.name == "CENTER")
|
|
assert center.color == 1 and center.is_frozen is False, "frozen was not requested"
|
|
|
|
|
|
async def test_restore_reports_missing_and_new_layers_and_never_creates(backend):
|
|
await _seed(backend)
|
|
await backend.layer_state_save("S")
|
|
await backend.layer_delete("CENTER")
|
|
await backend.layer_create("NEW-ONE")
|
|
result = await backend.layer_state_restore("S")
|
|
assert result["missing_layers"] == ["CENTER"] and result["new_layers"] == ["NEW-ONE"]
|
|
assert "CENTER" not in [lyr.name for lyr in await backend.layer_list()]
|
|
|
|
|
|
async def test_plot_flag_and_current_layer_round_trip(backend):
|
|
await _seed(backend)
|
|
backend._doc.layers.get("HIDDEN").dxf.plot = 0
|
|
await backend.layer_state_save("S")
|
|
backend._doc.layers.get("HIDDEN").dxf.plot = 1
|
|
await backend.layer_set_current("0")
|
|
await backend.layer_state_restore("S", properties=["plot", "current"])
|
|
assert backend._doc.layers.get("HIDDEN").dxf.plot == 0
|
|
assert backend._current_layer == "GEOMETRY"
|
|
assert backend._doc.header["$CLAYER"] == "GEOMETRY"
|
|
|
|
|
|
async def test_state_survives_save_and_reopen(backend, tmp_path):
|
|
await _seed(backend)
|
|
await backend.layer_state_save("PLOT", description="d")
|
|
path = tmp_path / "states.dxf"
|
|
await backend.drawing_save(str(path))
|
|
await backend.document_close(None)
|
|
await backend.drawing_open(str(path))
|
|
assert await backend.layer_state_list() == [
|
|
{"name": "PLOT", "description": "d", "layer_count": 5}
|
|
]
|
|
await backend.layer_thaw("CENTER")
|
|
await backend.layer_state_restore("PLOT")
|
|
center = next(lyr for lyr in await backend.layer_list() if lyr.name == "CENTER")
|
|
assert center.is_frozen is True
|
|
|
|
|
|
async def test_xrecord_chunks_stay_under_256_characters(backend):
|
|
for i in range(80):
|
|
await backend.layer_create(f"LONG-LAYER-NAME-{i:03d}-{'x' * 30}")
|
|
saved = await backend.layer_state_save("BIG")
|
|
assert saved["chunks"] > 10
|
|
xrecord = backend._doc.rootdict.get(DICT_NAME).get("BIG")
|
|
tags = [t for t in xrecord.tags if t.code == 1000]
|
|
assert len(tags) == saved["chunks"]
|
|
assert all(len(t.value) <= 255 for t in tags)
|
|
|
|
|
|
async def test_replace_delete_and_missing_names(backend):
|
|
await backend.layer_state_save("S")
|
|
again = await backend.layer_state_save("s")
|
|
assert again["replaced"] is True, "AutoCAD dictionaries are case-insensitive"
|
|
assert [row["name"] for row in await backend.layer_state_list()] == ["s"]
|
|
assert await backend.layer_state_delete("S") == {"ok": True, "deleted": "s", "backend": "ezdxf"}
|
|
assert await backend.layer_state_list() == []
|
|
with pytest.raises(ValueError, match="no layer state named 'S'"):
|
|
await backend.layer_state_restore("S")
|
|
with pytest.raises(ValueError, match="no layer state named 'S'"):
|
|
await backend.layer_state_delete("S")
|
|
|
|
|
|
async def test_refusals_happen_before_any_write(backend):
|
|
with pytest.raises(ValueError, match="layer state name"):
|
|
await backend.layer_state_save(" ")
|
|
with pytest.raises(TypeError, match="description"):
|
|
await backend.layer_state_save("S", description=3)
|
|
assert DICT_NAME not in backend._doc.rootdict, "a refused save creates no dictionary"
|
|
await backend.layer_state_save("S")
|
|
with pytest.raises(ValueError, match=r"properties\[0\]: 'plottable'"):
|
|
await backend.layer_state_restore("S", properties=["plottable"])
|
|
assert len(list(backend._doc.rootdict.get(DICT_NAME).keys())) == 1
|
|
|
|
|
|
# ── live engine, against a fake ActiveX surface ──────────────────────────────
|
|
|
|
|
|
class _FakeXRecord:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.types = None
|
|
self.values = None
|
|
self.deleted = False
|
|
|
|
def SetXRecordData(self, types_, values):
|
|
self.types, self.values = list(types_), list(values)
|
|
|
|
def GetXRecordData(self):
|
|
return self.types, self.values
|
|
|
|
def Delete(self):
|
|
self.deleted = True
|
|
|
|
|
|
class _Narrowed:
|
|
"""What pywin32 hands back for ``IAcadDictionaries.Item`` and
|
|
``IAcadDictionary.Item`` once a makepy cache exists: the gen_py
|
|
``IAcadObject`` wrapper of the *declared* return type — no ``Count``, no
|
|
``GetXRecordData`` — with the live object reachable only through
|
|
``_oleobj_`` (measured 2026-09-22, AutoCAD 2026, ``layer_state_restore``
|
|
in ``scripts/smoke_settings_com.py``: ``AttributeError: 'IAcadObject'
|
|
object has no attribute 'Count'``)."""
|
|
|
|
def __init__(self, obj):
|
|
self._oleobj_ = obj
|
|
|
|
def __getattr__(self, name):
|
|
raise AttributeError(f"'IAcadObject' object has no attribute {name!r}")
|
|
|
|
|
|
class _FakeDict:
|
|
def __init__(self, name):
|
|
self.Name = name
|
|
self.entries: list[_FakeXRecord] = []
|
|
self.calls: list[tuple[str, tuple]] = []
|
|
|
|
@property
|
|
def Count(self):
|
|
return len(self.entries)
|
|
|
|
def Item(self, index):
|
|
return _Narrowed(self.entries[index])
|
|
|
|
def GetName(self, obj):
|
|
return obj.name
|
|
|
|
def AddXRecord(self, keyword):
|
|
self.calls.append(("AddXRecord", (keyword,)))
|
|
rec = _FakeXRecord(keyword)
|
|
self.entries.append(rec)
|
|
return rec
|
|
|
|
def Remove(self, name):
|
|
self.calls.append(("Remove", (name,)))
|
|
rec = next(e for e in self.entries if e.name == name)
|
|
self.entries.remove(rec)
|
|
return rec
|
|
|
|
|
|
class _FakeDictionaries:
|
|
def __init__(self):
|
|
self.dicts: dict[str, _FakeDict] = {}
|
|
self.calls: list[tuple[str, tuple]] = []
|
|
|
|
def Item(self, name):
|
|
self.calls.append(("Item", (name,)))
|
|
if name not in self.dicts:
|
|
raise RuntimeError(f"no dictionary {name}")
|
|
return _Narrowed(self.dicts[name])
|
|
|
|
def Add(self, name):
|
|
self.calls.append(("Add", (name,)))
|
|
self.dicts[name] = _FakeDict(name)
|
|
return self.dicts[name]
|
|
|
|
|
|
class _FakeLayer:
|
|
"""Models the three refusals measured live (AutoCAD 2026): the active layer
|
|
cannot be frozen, a frozen layer cannot be made active, and layer ``0``
|
|
refuses the ``Freeze`` put with 'Invalid layer' whatever the value — even
|
|
``False`` on the already-thawed layer (2026-09-22, scratch document:
|
|
``Freeze=False`` on ``0`` → 'Invalid layer'; ``color`` / ``Linetype`` /
|
|
``Lineweight`` / ``Plottable`` / ``LayerOn`` / ``Lock`` / ``ActiveLayer``
|
|
all fine on it)."""
|
|
|
|
def __init__(self, name, **props):
|
|
self.Name = name
|
|
self.color = props.get("color", 7)
|
|
self.Linetype = props.get("linetype", "Continuous")
|
|
self.Lineweight = props.get("lineweight", -3)
|
|
self.LayerOn = props.get("on", True)
|
|
self._frozen = props.get("frozen", False)
|
|
self.Lock = props.get("locked", False)
|
|
self.Plottable = props.get("plot", True)
|
|
self.document = None
|
|
|
|
@property
|
|
def Freeze(self):
|
|
return self._frozen
|
|
|
|
@Freeze.setter
|
|
def Freeze(self, value):
|
|
if self.Name == "0":
|
|
raise RuntimeError("AutoCAD COM error: Invalid layer")
|
|
if value and self.document is not None and self.document.ActiveLayer is self:
|
|
raise RuntimeError("AutoCAD COM error: cannot freeze the current layer")
|
|
self._frozen = bool(value)
|
|
|
|
|
|
class _FakeDocument:
|
|
def __init__(self, layers, current):
|
|
self.Dictionaries = _FakeDictionaries()
|
|
self.Layers = _FakeLayers(layers)
|
|
self._active = current
|
|
for lyr in layers:
|
|
lyr.document = self
|
|
|
|
@property
|
|
def ActiveLayer(self):
|
|
return self._active
|
|
|
|
@ActiveLayer.setter
|
|
def ActiveLayer(self, layer):
|
|
if layer.Freeze:
|
|
# Measured: com_error 'Error setting active layer' (-2145320874).
|
|
raise RuntimeError("AutoCAD COM error (-0x7ffdfff7): Error setting active layer")
|
|
self._active = layer
|
|
|
|
|
|
class _FakeLayers:
|
|
def __init__(self, layers):
|
|
self._layers = layers
|
|
|
|
@property
|
|
def Count(self):
|
|
return len(self._layers)
|
|
|
|
def Item(self, key):
|
|
if isinstance(key, int):
|
|
return self._layers[key]
|
|
for lyr in self._layers:
|
|
if lyr.Name == key:
|
|
return lyr
|
|
raise RuntimeError(f"no layer {key}")
|
|
|
|
|
|
@pytest.fixture
|
|
def com_backend(monkeypatch):
|
|
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
|
from backends import com_backend as module
|
|
|
|
layers = [
|
|
_FakeLayer("0"),
|
|
_FakeLayer("GEOMETRY", lineweight=50),
|
|
_FakeLayer("HIDDEN", color=8, linetype="HIDDEN", frozen=True, plot=False),
|
|
]
|
|
document = _FakeDocument(layers, current=layers[1])
|
|
monkeypatch.setattr(module, "_acad_doc", lambda: document)
|
|
monkeypatch.setattr(module, "_ensure_linetype_loaded", lambda name: None)
|
|
monkeypatch.setattr(module, "_regen", lambda: None)
|
|
monkeypatch.setattr(module, "_ai", lambda values: list(values))
|
|
monkeypatch.setattr(module, "_avar", lambda values: list(values))
|
|
backend = module.ComBackend()
|
|
|
|
async def _run_inline(func, *args, **kwargs):
|
|
return func(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(backend, "_run", _run_inline)
|
|
return backend, document
|
|
|
|
|
|
async def test_com_save_creates_the_dictionary_and_one_xrecord_of_1000_chunks(com_backend):
|
|
backend, document = com_backend
|
|
result = await backend.layer_state_save("PLOT", description="d")
|
|
assert result["ok"] and result["backend"] == "com" and result["replaced"] is False
|
|
assert document.Dictionaries.calls == [("Item", (DICT_NAME,)), ("Add", (DICT_NAME,))]
|
|
states = document.Dictionaries.dicts[DICT_NAME]
|
|
assert states.calls == [("AddXRecord", ("PLOT",))]
|
|
(rec,) = states.entries
|
|
assert rec.types == [1000] * len(rec.values)
|
|
assert all(len(v) <= 255 for v in rec.values)
|
|
state = decode_state(rec.values)
|
|
assert state["current_layer"] == "GEOMETRY"
|
|
assert state["layers"]["HIDDEN"]["plot"] is False
|
|
again = await backend.layer_state_save("plot")
|
|
assert again["replaced"] is True and again["name"] == "PLOT"
|
|
assert len(states.entries) == 1, "SetXRecordData on the existing record, no second AddXRecord"
|
|
|
|
|
|
async def test_com_restore_reads_the_xrecord_and_writes_layer_properties(com_backend):
|
|
backend, document = com_backend
|
|
await backend.layer_state_save("S")
|
|
hidden = document.Layers.Item("HIDDEN")
|
|
hidden.Freeze = False
|
|
hidden.color = 3
|
|
hidden.Plottable = True
|
|
document.ActiveLayer = document.Layers.Item("0")
|
|
result = await backend.layer_state_restore("S")
|
|
assert result["applied"]["layers"] == 3
|
|
assert result["applied"]["current_layer"] == "GEOMETRY"
|
|
assert hidden.Freeze is True and hidden.color == 8 and hidden.Plottable is False
|
|
assert document.ActiveLayer.Name == "GEOMETRY"
|
|
assert result["missing_layers"] == [] and result["new_layers"] == []
|
|
|
|
|
|
async def test_com_restore_thaws_the_states_current_layer_before_making_it_current(com_backend):
|
|
"""The spec's own acceptance path, measured live: save with GEOMETRY
|
|
current and HIDDEN frozen → thaw HIDDEN, make 0 current, freeze GEOMETRY
|
|
→ restore. AutoCAD refuses ``ActiveLayer = <frozen>``, so the write
|
|
before the loop raised an opaque COM error and nothing was applied."""
|
|
backend, document = com_backend
|
|
await backend.layer_state_save("PLOT")
|
|
geometry = document.Layers.Item("GEOMETRY")
|
|
hidden = document.Layers.Item("HIDDEN")
|
|
hidden.Freeze = False
|
|
document.ActiveLayer = document.Layers.Item("0")
|
|
geometry.Freeze = True
|
|
result = await backend.layer_state_restore("PLOT")
|
|
assert "warnings" not in result, result
|
|
assert document.ActiveLayer is geometry and geometry.Freeze is False
|
|
assert hidden.Freeze is True
|
|
assert result["applied"] == {
|
|
"layers": 3,
|
|
"properties": list(PROPERTIES),
|
|
"current_layer": "GEOMETRY",
|
|
}
|
|
|
|
|
|
async def test_com_restore_reports_a_refused_current_layer_instead_of_failing(com_backend):
|
|
"""Without ``frozen`` in the requested properties the thaw is not ours to
|
|
make: AutoCAD's refusal lands in ``warnings`` and the rest still applies."""
|
|
backend, document = com_backend
|
|
await backend.layer_state_save("PLOT")
|
|
geometry = document.Layers.Item("GEOMETRY")
|
|
hidden = document.Layers.Item("HIDDEN")
|
|
hidden.color = 3
|
|
document.ActiveLayer = document.Layers.Item("0")
|
|
geometry.Freeze = True
|
|
result = await backend.layer_state_restore("PLOT", properties=["color", "current"])
|
|
assert result["applied"]["current_layer"] is None
|
|
assert result["applied"]["layers"] == 3 and hidden.color == 8
|
|
assert geometry.Freeze is True and document.ActiveLayer.Name == "0"
|
|
(warning,) = result["warnings"]
|
|
assert warning.startswith("GEOMETRY: cannot be made current: ")
|
|
|
|
|
|
async def test_com_restore_never_writes_freeze_on_layer_zero(com_backend):
|
|
"""Measured live (see ``_FakeLayer``): the ``Freeze`` put on layer ``0``
|
|
raises 'Invalid layer' even for ``False``, so a state saved with ``0``
|
|
current used to come back with two warnings and ``0`` unapplied. A
|
|
``Freeze`` write only happens when it would change the layer."""
|
|
backend, document = com_backend
|
|
document.ActiveLayer = document.Layers.Item("0")
|
|
await backend.layer_state_save("ZERO")
|
|
hidden = document.Layers.Item("HIDDEN")
|
|
hidden.color = 3
|
|
document.ActiveLayer = document.Layers.Item("GEOMETRY")
|
|
result = await backend.layer_state_restore("ZERO")
|
|
assert "warnings" not in result, result
|
|
assert result["applied"] == {
|
|
"layers": 3,
|
|
"properties": list(PROPERTIES),
|
|
"current_layer": "0",
|
|
}
|
|
assert document.ActiveLayer.Name == "0" and hidden.color == 8
|
|
|
|
|
|
async def test_com_list_and_delete(com_backend):
|
|
backend, document = com_backend
|
|
await backend.layer_state_save("A", description="one")
|
|
await backend.layer_state_save("B")
|
|
assert await backend.layer_state_list() == [
|
|
{"name": "A", "description": "one", "layer_count": 3},
|
|
{"name": "B", "description": None, "layer_count": 3},
|
|
]
|
|
assert (await backend.layer_state_delete("a"))["deleted"] == "A"
|
|
states = document.Dictionaries.dicts[DICT_NAME]
|
|
assert states.calls[-1] == ("Remove", ("A",))
|
|
with pytest.raises(ValueError, match="no layer state named 'A'"):
|
|
await backend.layer_state_delete("A")
|