fix(task-17): COM page setup reads mm as mm, 1:5 takes the custom route, and a refused apply puts the media back
Three live-measured behaviours of AutoCAD 2026 that the fake did not replay: * GetPaperSize / GetPaperMargins answer in millimetres whatever PaperUnits says; the 25.4 factor listed every imperial layout as [10668.0, 7543.8] and lost the paper name. No factor now; the fake pins PaperUnits=0. * Layout.StandardScale = 19 (ac1_5) is refused with "Invalid input" in every condition; '1:5' is dropped from ACTIVEX_PLOT_SCALE so it goes through SetCustomScale(1, 5) like 5:1 already did. * Writing ConfigName replaces CanonicalMediaName with the new device's default, and writing the old device back does not bring the media back — the unknown-media refusal and a late refusal both left the layout on the old device's default paper while the error claimed it was restored. The device write now snapshots media / units / rotation and journals one combined undo; the read-back after every unwind is compared to the read-back before the first write and the message names what did not go back instead of asserting it did. 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
20ca39d28a
commit
376dc812a5
+120
-5
@@ -109,7 +109,9 @@ def test_resolve_page_setup_accepts_lowercase_and_short_paper_names():
|
||||
def test_resolve_page_setup_scale_codes():
|
||||
one_to_five = resolve_page_setup("ISO_A3", scale="1:5")
|
||||
assert one_to_five["dxf_standard_scale_type"] is None # DXF code 75 has no 1:5
|
||||
assert one_to_five["activex_standard_scale"] == 19 # AcPlotScale.ac1_5
|
||||
# The typelib declares ac1_5 = 19, but AutoCAD 2026 refuses
|
||||
# StandardScale = 19 ("Invalid input", measured live): custom route.
|
||||
assert one_to_five["activex_standard_scale"] is None
|
||||
assert one_to_five["scale_ratio"] == [1.0, 5.0]
|
||||
five_to_one = resolve_page_setup("ISO_A3", scale="5:1")
|
||||
assert five_to_one["dxf_standard_scale_type"] is None
|
||||
@@ -420,13 +422,24 @@ class _FakeLayout:
|
||||
be set to True on a layout object whose PlotType property is set to
|
||||
acLayout." ``refuse`` names properties whose write raises, the way a live
|
||||
AutoCAD refuses a value it cannot take.
|
||||
|
||||
Replays two behaviours measured live on AutoCAD 2026 (2026-09-17):
|
||||
|
||||
* ``GetPaperSize`` / ``GetPaperMargins`` answer in millimetres whatever
|
||||
``PaperUnits`` says (ANSI_B under PaperUnits 0 and 1 -> (431.8, 279.4)).
|
||||
* Writing ``ConfigName`` replaces ``CanonicalMediaName`` with the new
|
||||
device's default when the current media does not exist on it, and
|
||||
writing the old device back does not bring the old media back.
|
||||
``media_names`` is therefore either one tuple (every device carries it)
|
||||
or ``{device: tuple}``; a device missing from the dict carries nothing.
|
||||
"""
|
||||
|
||||
AC_LAYOUT = 5
|
||||
|
||||
def __init__(self, name, media_names, refuse=()):
|
||||
object.__setattr__(self, "log", [])
|
||||
object.__setattr__(self, "_media", tuple(media_names))
|
||||
media = media_names if isinstance(media_names, dict) else {None: tuple(media_names)}
|
||||
object.__setattr__(self, "_media", {k: tuple(v) for k, v in media.items()})
|
||||
object.__setattr__(self, "_refuse", tuple(refuse))
|
||||
object.__setattr__(
|
||||
self,
|
||||
@@ -459,6 +472,14 @@ class _FakeLayout:
|
||||
raise RuntimeError("CenterPlot cannot be True while PlotType is acLayout")
|
||||
self._props[attr] = value
|
||||
self.log.append(("set", attr, value))
|
||||
if attr == "ConfigName":
|
||||
names = self._device_media()
|
||||
if names and self._props["CanonicalMediaName"] not in names:
|
||||
self._props["CanonicalMediaName"] = names[0] # the device's default
|
||||
|
||||
def _device_media(self):
|
||||
media = object.__getattribute__(self, "_media")
|
||||
return media.get(self._props["ConfigName"], media.get(None, ()))
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
return dict(object.__getattribute__(self, "_props"))
|
||||
@@ -468,13 +489,14 @@ class _FakeLayout:
|
||||
|
||||
def GetCanonicalMediaNames(self):
|
||||
self.log.append(("call", "GetCanonicalMediaNames"))
|
||||
return self._media
|
||||
return self._device_media()
|
||||
|
||||
def GetPaperSize(self):
|
||||
# Millimetres whatever PaperUnits says (measured live, see the class docstring).
|
||||
match = _MEDIA_RE.search(self._props["CanonicalMediaName"])
|
||||
width, height = float(match.group(1)), float(match.group(2))
|
||||
if match.group(3) == "Inches":
|
||||
width, height = width * 25.4, height * 25.4
|
||||
width, height = round(width * 25.4, 2), round(height * 25.4, 2)
|
||||
return (width, height)
|
||||
|
||||
def GetPaperMargins(self):
|
||||
@@ -631,13 +653,90 @@ async def test_com_apply_refuses_margins_before_any_write(com_backend):
|
||||
async def test_com_apply_restores_the_device_when_the_media_name_is_unknown(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
object.__setattr__(layout, "_media", ("ISO_A4_(210.00_x_297.00_MM)",))
|
||||
object.__setattr__(layout, "_media", {None: ("ISO_A4_(210.00_x_297.00_MM)",)})
|
||||
with pytest.raises(ValueError, match=r"ISO_A3_\(420.00_x_297.00_MM\)"):
|
||||
await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A3"))
|
||||
assert layout.ConfigName == "None", "the device write is rolled back"
|
||||
assert layout.CanonicalMediaName == "ISO_A4_(210.00_x_297.00_MM)"
|
||||
|
||||
|
||||
PDF_DEVICE = "DWG To PDF.pc3"
|
||||
MS_DEVICE = "Microsoft Print to PDF"
|
||||
ISO_A3_MEDIA = "ISO_A3_(420.00_x_297.00_MM)"
|
||||
#: Two devices whose media do not overlap, the way the two PDF drivers on the
|
||||
#: machine measured: switching to MS moves an ISO_A3 layout to psk:ISOA4, and
|
||||
#: switching back lands on DWG To PDF's default, not on ISO_A3.
|
||||
TWO_DEVICES = {
|
||||
PDF_DEVICE: ("ANSI_A_(11.00_x_8.50_Inches)", ISO_A3_MEDIA),
|
||||
MS_DEVICE: ("psk:ISOA4", "ISO_A0_(1189.00_x_841.00_MM)"),
|
||||
}
|
||||
|
||||
|
||||
def _layout_on_pdf_device(document, **kwargs):
|
||||
layout = _FakeLayout("Layout1", TWO_DEVICES, **kwargs)
|
||||
layout.ConfigName = PDF_DEVICE
|
||||
layout.CanonicalMediaName = ISO_A3_MEDIA
|
||||
del layout.log[:]
|
||||
document.Layouts.layouts[1] = layout
|
||||
return layout
|
||||
|
||||
|
||||
async def test_com_apply_puts_the_media_back_when_the_device_switch_replaced_it(com_backend):
|
||||
"""Measured live: refusing a media unknown on the new device used to leave
|
||||
the layout on the old device with that device's *default* media (ANSI_A),
|
||||
not the ISO_A3 it had — while the error said it was restored."""
|
||||
backend, document = com_backend
|
||||
layout = _layout_on_pdf_device(document)
|
||||
before = layout.snapshot()
|
||||
with pytest.raises(ValueError, match="has no media") as info:
|
||||
await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A2", device=MS_DEVICE))
|
||||
assert "could NOT" not in str(info.value)
|
||||
assert layout.snapshot() == before, "device back, then the original media re-applied"
|
||||
assert (await backend.page_setup_list("Layout1"))[0]["paper"] == "ISO_A3"
|
||||
|
||||
|
||||
async def test_com_apply_late_refusal_unwinds_through_the_device_switch(com_backend):
|
||||
"""The media journaled *after* the device switch is the new device's
|
||||
default; only the pre-switch snapshot can put ISO_A3 back."""
|
||||
backend, document = com_backend
|
||||
layout = _layout_on_pdf_device(document, refuse=("PlotWithPlotStyles",))
|
||||
before = layout.snapshot()
|
||||
with pytest.raises(ValueError, match="PlotWithPlotStyles") as info:
|
||||
await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A0", device=MS_DEVICE))
|
||||
assert "restored to its previous page setup" in str(info.value)
|
||||
assert layout.snapshot() == before
|
||||
sets = [entry for entry in layout.log if entry[0] == "set"]
|
||||
assert sets[-2:] == [
|
||||
("set", "ConfigName", PDF_DEVICE),
|
||||
("set", "CanonicalMediaName", ISO_A3_MEDIA),
|
||||
]
|
||||
|
||||
|
||||
class _StickyMediaLayout(_FakeLayout):
|
||||
"""Accepts every write but silently drops CanonicalMediaName once the device
|
||||
has been switched back — what a driver that ignores the write looks like."""
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr == "CanonicalMediaName" and self._props["ConfigName"] == PDF_DEVICE and self.log:
|
||||
return
|
||||
super().__setattr__(attr, value)
|
||||
|
||||
|
||||
async def test_com_apply_reports_what_the_unwind_could_not_put_back(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = _StickyMediaLayout("Layout1", TWO_DEVICES)
|
||||
layout.ConfigName = PDF_DEVICE
|
||||
object.__getattribute__(layout, "_props")["CanonicalMediaName"] = ISO_A3_MEDIA
|
||||
del layout.log[:]
|
||||
document.Layouts.layouts[1] = layout
|
||||
with pytest.raises(ValueError, match="has no media") as info:
|
||||
await backend.page_setup_apply("Layout1", resolve_page_setup("ISO_A2", device=MS_DEVICE))
|
||||
message = str(info.value)
|
||||
assert "could NOT be fully restored" in message
|
||||
assert "canonical_media_name" in message and "paper" in message
|
||||
assert layout.ConfigName == PDF_DEVICE and layout.CanonicalMediaName != ISO_A3_MEDIA
|
||||
|
||||
|
||||
async def test_com_apply_refuses_model_and_unknown_layouts(com_backend):
|
||||
backend, _ = com_backend
|
||||
with pytest.raises(ValueError, match="Model"):
|
||||
@@ -659,6 +758,22 @@ async def test_com_list_reads_the_properties(com_backend):
|
||||
assert row["center"] is None, "acLayout: CenterPlot is not applicable"
|
||||
|
||||
|
||||
async def test_com_list_never_scales_the_paper_by_paper_units(com_backend):
|
||||
"""GetPaperSize / GetPaperMargins are millimetres under PaperUnits=0 too
|
||||
(measured live) — the 25.4 factor listed every imperial layout as
|
||||
[10668.0, 7543.8] and lost the paper name."""
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
layout.PaperUnits = 0
|
||||
layout.CanonicalMediaName = "ANSI_B_(17.00_x_11.00_Inches)"
|
||||
row = (await backend.page_setup_list("Layout1"))[0]
|
||||
assert row["size_mm"] == [431.8, 279.4] and row["paper"] == "ANSI_B"
|
||||
assert row["orientation"] == "landscape"
|
||||
assert row["margins_mm"] == [20.0, 20.0, 7.5, 7.5]
|
||||
layout.CanonicalMediaName = "ISO_A3_(420.00_x_297.00_MM)"
|
||||
assert (await backend.page_setup_list("Layout1"))[0]["size_mm"] == [420.0, 297.0]
|
||||
|
||||
|
||||
async def test_com_list_reports_center_for_an_extents_plot(com_backend):
|
||||
backend, document = com_backend
|
||||
layout = document.Layouts.Item("Layout1")
|
||||
|
||||
Reference in New Issue
Block a user