merge: track-e group P
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
"""batch_plot: every sheet to PDF, each sheet size read back from the PDF.
|
||||
|
||||
The composition is exercised on the headless engine end to end; the COM-only
|
||||
pieces of this task (the installed ctb scan, and the plot call's shape —
|
||||
foreground, by layout name, confirmed on disk) are fake-tested; the SECTION 19
|
||||
tools are called directly with a minimal context.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
import server
|
||||
from engineering.standards.papers import CTB_CATALOG, resolve_page_setup
|
||||
from engineering.standards.plot import batch_plot, safe_filename
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class _Ctx:
|
||||
def __init__(self, backend):
|
||||
self.lifespan_context = {"backend": backend}
|
||||
|
||||
async def info(self, message):
|
||||
pass
|
||||
|
||||
async def warning(self, message):
|
||||
pass
|
||||
|
||||
async def report_progress(self, progress, total):
|
||||
pass
|
||||
|
||||
|
||||
async def _two_sheets(backend):
|
||||
"""Exactly two paper layouts: the default tab renamed, plus one more."""
|
||||
await backend.layout_rename("Layout1", "A3")
|
||||
await backend.layout_create("B")
|
||||
await backend.page_setup_apply("A3", resolve_page_setup("ISO_A3", "landscape"))
|
||||
await backend.page_setup_apply("B", resolve_page_setup("ANSI_B", "landscape"))
|
||||
await backend.entity_create_line(0, 0, 100, 50)
|
||||
|
||||
|
||||
def test_safe_filename_strips_what_a_filesystem_refuses():
|
||||
assert safe_filename("Sheet 1: A3/rev*B") == "Sheet_1_A3_rev_B"
|
||||
assert safe_filename("Layout1") == "Layout1"
|
||||
|
||||
|
||||
async def test_every_paper_layout_is_plotted_and_measured(backend, tmp_path):
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
await _two_sheets(backend)
|
||||
result = await batch_plot(backend, None, str(tmp_path / "out"))
|
||||
assert result["ok"] is True and result["format"] == "pdf"
|
||||
assert result["count"] == 2
|
||||
by_layout = {row["layout"]: row for row in result["sheets"]}
|
||||
assert set(by_layout) == {"A3", "B"}, "Model is not plotted unless named"
|
||||
a3, b = by_layout["A3"], by_layout["B"]
|
||||
assert a3["ok"] and a3["bytes"] > 0 and a3["path"].endswith("untitled-A3.pdf")
|
||||
assert abs(a3["mediabox_mm"][0] - 420) <= 0.5 and abs(a3["mediabox_mm"][1] - 297) <= 0.5
|
||||
assert a3["paper"] == "ISO_A3"
|
||||
assert abs(b["mediabox_mm"][0] - 432) <= 0.5 and abs(b["mediabox_mm"][1] - 279) <= 0.5
|
||||
assert b["paper"] == "ANSI_B"
|
||||
|
||||
|
||||
async def test_model_is_included_only_when_named(backend, tmp_path):
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
await _two_sheets(backend)
|
||||
result = await batch_plot(backend, ["Model", "A3"], str(tmp_path))
|
||||
names = [row["layout"] for row in result["sheets"]]
|
||||
assert names == ["Model", "A3"]
|
||||
model = result["sheets"][0]
|
||||
assert model["ok"] and model["mediabox_mm"] is not None
|
||||
assert model["paper"] is None, "model space has no sheet; the size is reported, not named"
|
||||
|
||||
|
||||
async def test_refusals_write_nothing(backend, tmp_path):
|
||||
await _two_sheets(backend)
|
||||
out = tmp_path / "nothing"
|
||||
with pytest.raises(ValueError, match="Nope"):
|
||||
await batch_plot(backend, ["Nope"], str(out))
|
||||
with pytest.raises(ValueError, match="layout"):
|
||||
await batch_plot(backend, None, str(out), file_pattern="all.pdf")
|
||||
with pytest.raises(ValueError, match="file_pattern"):
|
||||
await batch_plot(backend, None, str(out), file_pattern="sub/{layout}.pdf")
|
||||
with pytest.raises(ValueError, match="layouts"):
|
||||
await batch_plot(backend, [], str(out))
|
||||
assert not out.exists() or not any(out.iterdir())
|
||||
|
||||
|
||||
async def test_a_fresh_drawing_plots_its_one_default_sheet_at_the_dxf_default_size(
|
||||
backend, tmp_path
|
||||
):
|
||||
"""No page setup ever applied: the sheet still plots, at ezdxf's stored
|
||||
default (A3, 420 x 297), and the row says so rather than guessing."""
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
result = await batch_plot(backend, None, str(tmp_path), file_pattern="{layout}.pdf")
|
||||
assert [row["layout"] for row in result["sheets"]] == ["Layout1"]
|
||||
row = result["sheets"][0]
|
||||
assert row["path"].endswith("Layout1.pdf") and row["paper"] == "ISO_A3"
|
||||
|
||||
|
||||
async def _model_circle_and_sheet_rectangle(backend):
|
||||
"""Distinct geometry per space, told apart by the PDF's aspect ratio: a
|
||||
circle in model space (a square page) and a 390 x 270 rectangle on the A3
|
||||
sheet (a landscape page)."""
|
||||
await backend.entity_create_circle(50, 50, 25)
|
||||
await backend.layout_rename("Layout1", "A3")
|
||||
await backend.page_setup_apply("A3", resolve_page_setup("ISO_A3", "landscape"))
|
||||
await backend.layout_set_current("A3")
|
||||
await backend.entity_create_polyline([[10, 10], [400, 10], [400, 280], [10, 280]], closed=True)
|
||||
|
||||
|
||||
def _aspect(mediabox_mm):
|
||||
width, height = mediabox_mm
|
||||
return width / height
|
||||
|
||||
|
||||
async def test_the_model_row_is_model_space_even_when_a_sheet_is_current(backend, tmp_path):
|
||||
"""The Model row must be model space whichever tab is current — proven by
|
||||
content, not by the row's shape. The headless export used to route the
|
||||
model target through ``_msp()`` (the *current* tab), so with a sheet
|
||||
current the "Model" PDF was that sheet's paper-space content: measured
|
||||
176.1 x 121.9 mm (the rectangle's aspect) against 121.9 x 121.9 (the
|
||||
circle) with Model current. COM had the mirror defect (``ActiveLayout``
|
||||
for ``layout=None``). batch_plot names the layout for every row, Model
|
||||
included, and the engine honours the name."""
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
await _model_circle_and_sheet_rectangle(backend)
|
||||
seen: list[str | None] = []
|
||||
original = backend.drawing_export_pdf
|
||||
|
||||
async def _recording(path, layout=None):
|
||||
seen.append(layout)
|
||||
return await original(path, layout=layout)
|
||||
|
||||
backend.drawing_export_pdf = _recording
|
||||
with_sheet_current = await batch_plot(backend, ["Model", "A3"], str(tmp_path / "sheet"))
|
||||
assert seen == ["Model", "A3"]
|
||||
await backend.layout_set_current("Model")
|
||||
with_model_current = await batch_plot(backend, ["Model"], str(tmp_path / "model"))
|
||||
|
||||
model_row = with_sheet_current["sheets"][0]
|
||||
assert with_sheet_current["ok"] and model_row["paper"] is None
|
||||
assert model_row["mediabox_mm"] == pytest.approx(
|
||||
with_model_current["sheets"][0]["mediabox_mm"], abs=0.05
|
||||
), "the Model row changed with the current tab"
|
||||
assert _aspect(model_row["mediabox_mm"]) == pytest.approx(1.0, abs=0.02), (
|
||||
"the Model row is the circle (square page), not the sheet's rectangle"
|
||||
)
|
||||
a3_row = with_sheet_current["sheets"][1]
|
||||
assert a3_row["paper"] == "ISO_A3"
|
||||
assert _aspect(a3_row["mediabox_mm"]) == pytest.approx(420 / 297, abs=0.02)
|
||||
|
||||
|
||||
async def test_export_pdf_without_a_layout_is_model_space_even_when_a_sheet_is_current(
|
||||
backend, tmp_path
|
||||
):
|
||||
"""``drawing_export_pdf(path)`` advertises model space as its default; a
|
||||
current sheet must not turn it into that sheet."""
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
from engineering.standards.plot import read_mediabox
|
||||
|
||||
await _model_circle_and_sheet_rectangle(backend)
|
||||
default = tmp_path / "default.pdf"
|
||||
named = tmp_path / "named.pdf"
|
||||
assert (await backend.drawing_export_pdf(str(default)))["ok"]
|
||||
assert (await backend.drawing_export_pdf(str(named), layout="Model"))["ok"]
|
||||
assert read_mediabox(str(default)) == pytest.approx(read_mediabox(str(named)), abs=0.05)
|
||||
assert _aspect(read_mediabox(str(default))) == pytest.approx(1.0, abs=0.02)
|
||||
|
||||
|
||||
async def test_a_sheet_the_engine_cannot_plot_is_a_failed_row_not_a_lost_batch(backend, tmp_path):
|
||||
"""COM raises RuntimeError for an AutoCAD error (E_FAIL after a stranded
|
||||
background job, say). That is one row's failure; the sheets already
|
||||
written stay in the report with their evidence."""
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
await _two_sheets(backend)
|
||||
original = backend.drawing_export_pdf
|
||||
|
||||
async def _flaky(path, layout=None):
|
||||
if layout == "B":
|
||||
raise RuntimeError("AutoCAD COM error (-0x7ffdfff7): Unspecified error")
|
||||
return await original(path, layout=layout)
|
||||
|
||||
backend.drawing_export_pdf = _flaky
|
||||
result = await batch_plot(backend, None, str(tmp_path))
|
||||
assert result["ok"] is False and result["count"] == 2
|
||||
a3, b = result["sheets"]
|
||||
assert a3["ok"] and a3["paper"] == "ISO_A3"
|
||||
assert b == {
|
||||
"layout": "B",
|
||||
"ok": False,
|
||||
"path": str(tmp_path / "untitled-B.pdf"),
|
||||
"error": "AutoCAD COM error (-0x7ffdfff7): Unspecified error",
|
||||
}
|
||||
|
||||
|
||||
async def test_a_plot_reported_ok_without_a_file_is_a_failed_row(backend, tmp_path):
|
||||
"""A background plot answers ok before any file exists; the row says so
|
||||
instead of dying on ``path.stat()`` with an unhandled FileNotFoundError."""
|
||||
await _two_sheets(backend)
|
||||
|
||||
async def _phantom(path, layout=None):
|
||||
return {"ok": True, "path": path}
|
||||
|
||||
backend.drawing_export_pdf = _phantom
|
||||
result = await batch_plot(backend, ["A3"], str(tmp_path))
|
||||
assert result["ok"] is False
|
||||
row = result["sheets"][0]
|
||||
assert row["ok"] is False and "no file" in row["error"]
|
||||
assert "bytes" not in row and "mediabox_mm" not in row
|
||||
|
||||
|
||||
# ── the tools ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_page_setup_tools_round_trip(backend):
|
||||
ctx = _Ctx(backend)
|
||||
# A fresh drawing already carries Layout1; renaming it keeps exactly one
|
||||
# sheet, so the unfiltered listing has one row and it is the A3.
|
||||
await backend.layout_rename("Layout1", "A3")
|
||||
applied = await server.page_setup_apply("A3", "ISO_A3", ctx=ctx)
|
||||
assert applied["ok"] and applied["applied"]["paper"] == "ISO_A3"
|
||||
listed = await server.page_setup_list(ctx=ctx)
|
||||
assert listed["count"] == 1 and listed["layouts"][0]["paper"] == "ISO_A3"
|
||||
only = await server.page_setup_list("A3", ctx=ctx)
|
||||
assert only["layouts"][0]["layout"] == "A3"
|
||||
|
||||
|
||||
async def test_page_setup_apply_tool_refuses_before_writing(backend):
|
||||
ctx = _Ctx(backend)
|
||||
await backend.layout_create("A3")
|
||||
with pytest.raises(ToolError, match="scale"):
|
||||
await server.page_setup_apply("A3", "ISO_A3", scale="1:3", ctx=ctx)
|
||||
with pytest.raises(ToolError, match="Model"):
|
||||
await server.page_setup_apply("Model", "ISO_A3", ctx=ctx)
|
||||
row = (await backend.page_setup_list("A3"))[0]
|
||||
assert row["device"] == "", "nothing was written"
|
||||
|
||||
|
||||
async def test_plot_style_list_tool(backend):
|
||||
result = await server.plot_style_list(ctx=_Ctx(backend))
|
||||
assert result["ok"] and result["count"] == len(CTB_CATALOG)
|
||||
assert result["backend"] == "ezdxf"
|
||||
|
||||
|
||||
async def test_batch_plot_tool_validates_the_output_directory(backend, tmp_path):
|
||||
pytest.importorskip("matplotlib", reason="rendering needs the [pdf] extra")
|
||||
await _two_sheets(backend)
|
||||
with pytest.raises(ToolError):
|
||||
await server.batch_plot("../../outside", ctx=_Ctx(backend))
|
||||
with pytest.raises(ToolError, match="format"):
|
||||
await server.batch_plot(str(tmp_path), format="png", ctx=_Ctx(backend))
|
||||
result = await server.batch_plot(str(tmp_path), layouts=["A3"], ctx=_Ctx(backend))
|
||||
assert result["count"] == 1 and result["sheets"][0]["paper"] == "ISO_A3"
|
||||
|
||||
|
||||
# ── COM: the installed ctb scan ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_backend(monkeypatch, tmp_path):
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
from backends import com_backend as module
|
||||
|
||||
styles = tmp_path / "Plot Styles"
|
||||
styles.mkdir()
|
||||
(styles / "monochrome.ctb").write_bytes(b"")
|
||||
(styles / "Anka.ctb").write_bytes(b"")
|
||||
(styles / "readme.txt").write_bytes(b"")
|
||||
app = types.SimpleNamespace(
|
||||
Preferences=types.SimpleNamespace(
|
||||
Files=types.SimpleNamespace(PrinterStyleSheetPath=f"{styles};C:\\does\\not\\exist")
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
||||
backend = module.ComBackend()
|
||||
|
||||
async def _run_inline(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||
return backend, app
|
||||
|
||||
|
||||
async def test_com_plot_style_list_marks_installed_and_adds_local_files(com_backend):
|
||||
backend, _ = com_backend
|
||||
rows = await backend.plot_style_list()
|
||||
by_name = {row["name"]: row for row in rows}
|
||||
assert by_name["monochrome.ctb"] == {
|
||||
"name": "monochrome.ctb",
|
||||
"source": "catalog",
|
||||
"installed": True,
|
||||
}
|
||||
assert by_name["acad.ctb"]["installed"] is False
|
||||
assert by_name["Anka.ctb"] == {"name": "Anka.ctb", "source": "installed", "installed": True}
|
||||
assert "readme.txt" not in by_name
|
||||
|
||||
|
||||
async def test_com_plot_style_list_survives_an_unreachable_preferences_object(
|
||||
com_backend, monkeypatch
|
||||
):
|
||||
backend, _ = com_backend
|
||||
from backends import com_backend as module
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("no application")
|
||||
|
||||
monkeypatch.setattr(module, "_acad_app", _boom)
|
||||
rows = await backend.plot_style_list()
|
||||
assert [row["name"] for row in rows] == list(CTB_CATALOG)
|
||||
assert all(row["installed"] is None for row in rows)
|
||||
|
||||
|
||||
# ── COM: the plot call's shape ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_plot(monkeypatch):
|
||||
"""A fake document whose ``Plot.PlotToFile`` records the active layout and
|
||||
BACKGROUNDPLOT at the moment of the plot — the two things AutoCAD acts on
|
||||
and the reviewer measured wrong: with a sheet current, ``layout=None``
|
||||
plotted that sheet; with the operator's BACKGROUNDPLOT=2 the file did not
|
||||
exist when the call returned."""
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
from backends import com_backend as module
|
||||
|
||||
state = {
|
||||
"vars": {"BACKGROUNDPLOT": 2},
|
||||
"sets": [],
|
||||
"active": "Layout1",
|
||||
"plots": [],
|
||||
"write_file": True,
|
||||
"plotted": True,
|
||||
}
|
||||
names = ["Model", "Layout1", "Layout2"]
|
||||
|
||||
class _Layout:
|
||||
def __init__(self, name):
|
||||
self.Name = name
|
||||
|
||||
class _Layouts:
|
||||
def Item(self, key):
|
||||
for name in names:
|
||||
if name.lower() == str(key).lower():
|
||||
return _Layout(name)
|
||||
raise KeyError(key)
|
||||
|
||||
class _Plot:
|
||||
def PlotToFile(self, path, config):
|
||||
state["plots"].append((state["active"], config, state["vars"]["BACKGROUNDPLOT"]))
|
||||
if state["write_file"]:
|
||||
Path(path).write_bytes(b"%PDF-1.4 fake")
|
||||
return state["plotted"]
|
||||
|
||||
class _Doc:
|
||||
Layouts = _Layouts()
|
||||
Plot = _Plot()
|
||||
|
||||
@property
|
||||
def ActiveLayout(self):
|
||||
return _Layout(state["active"])
|
||||
|
||||
@ActiveLayout.setter
|
||||
def ActiveLayout(self, layout):
|
||||
state["active"] = layout.Name
|
||||
|
||||
def GetVariable(self, name):
|
||||
return state["vars"][name]
|
||||
|
||||
def SetVariable(self, name, value):
|
||||
state["sets"].append((name, value))
|
||||
state["vars"][name] = value
|
||||
|
||||
monkeypatch.setattr(module, "_acad_doc", lambda: _Doc())
|
||||
backend = module.ComBackend()
|
||||
|
||||
async def _run_inline(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||
return backend, state
|
||||
|
||||
|
||||
async def test_com_export_pdf_plots_model_space_by_name_in_the_foreground(com_plot, tmp_path):
|
||||
backend, state = com_plot
|
||||
out = tmp_path / "model.pdf"
|
||||
result = await backend.drawing_export_pdf(str(out))
|
||||
assert state["plots"] == [("Model", "DWG To PDF.pc3", 0)], (
|
||||
"layout=None is model space, plotted with BACKGROUNDPLOT forced to 0"
|
||||
)
|
||||
assert result == {"ok": True, "path": str(out), "layout": "Model", "bytes": out.stat().st_size}
|
||||
assert state["active"] == "Layout1", "the sheet that was current is current again"
|
||||
assert state["vars"]["BACKGROUNDPLOT"] == 2, "the operator's setting is restored"
|
||||
assert state["sets"] == [("BACKGROUNDPLOT", 0), ("BACKGROUNDPLOT", 2)]
|
||||
|
||||
|
||||
async def test_com_export_pdf_plots_the_named_sheet_and_restores_the_tab(com_plot, tmp_path):
|
||||
backend, state = com_plot
|
||||
result = await backend.drawing_export_pdf(str(tmp_path / "l2.pdf"), layout="layout2")
|
||||
assert [row[0] for row in state["plots"]] == ["Layout2"]
|
||||
assert result["ok"] and result["layout"] == "layout2"
|
||||
assert state["active"] == "Layout1"
|
||||
|
||||
|
||||
async def test_com_export_pdf_leaves_backgroundplot_alone_when_already_foreground(
|
||||
com_plot, tmp_path
|
||||
):
|
||||
backend, state = com_plot
|
||||
state["vars"]["BACKGROUNDPLOT"] = 0
|
||||
await backend.drawing_export_pdf(str(tmp_path / "m.pdf"))
|
||||
assert state["sets"] == []
|
||||
|
||||
|
||||
async def test_com_export_pdf_reports_a_refused_plot(com_plot, tmp_path):
|
||||
backend, state = com_plot
|
||||
state["plotted"] = False
|
||||
state["write_file"] = False
|
||||
result = await backend.drawing_export_pdf(str(tmp_path / "no.pdf"), layout="Layout1")
|
||||
assert result["ok"] is False and "PlotToFile returned False" in result["error"]
|
||||
assert state["vars"]["BACKGROUNDPLOT"] == 2
|
||||
|
||||
|
||||
async def test_com_export_pdf_reports_a_plot_that_left_no_file(com_plot, tmp_path):
|
||||
"""PlotToFile answering True with nothing on disk is the background-plot
|
||||
signature; it is a failure, not an ok."""
|
||||
backend, state = com_plot
|
||||
state["write_file"] = False
|
||||
result = await backend.drawing_export_pdf(str(tmp_path / "gone.pdf"))
|
||||
assert result["ok"] is False and "wrote no file" in result["error"]
|
||||
assert "bytes" not in result
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,487 @@
|
||||
"""Bundled templates: catalogue, resolution, reproducible build, drawing_new(template=)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
import server
|
||||
from backends.base import UnsupportedCapabilityError
|
||||
from engineering.standards.templates import (
|
||||
TEMPLATE_CATALOG,
|
||||
TEMPLATES_DIR,
|
||||
resolve_template,
|
||||
template_rows,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
NAMES = ("iso_a3_mech", "iso_a1_arch", "iso_a3_pid", "ansi_b_mech", "ansi_d_arch")
|
||||
|
||||
|
||||
def _build_module():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"build_templates", ROOT / "scripts" / "build_templates.py"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class _Ctx:
|
||||
def __init__(self, backend):
|
||||
self.lifespan_context = {"backend": backend}
|
||||
|
||||
async def info(self, message):
|
||||
pass
|
||||
|
||||
async def warning(self, message):
|
||||
pass
|
||||
|
||||
async def report_progress(self, progress, total):
|
||||
pass
|
||||
|
||||
|
||||
# ── catalogue ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_catalog_has_the_five_spec_rows():
|
||||
assert tuple(TEMPLATE_CATALOG) == NAMES
|
||||
rows = {spec.name: spec for spec in TEMPLATE_CATALOG.values()}
|
||||
assert (rows["iso_a3_mech"].paper, rows["iso_a3_mech"].layer_set) == ("ISO_A3", "mech")
|
||||
assert (rows["iso_a1_arch"].paper, rows["iso_a1_arch"].layer_set) == ("ISO_A1", "iso13567")
|
||||
assert rows["iso_a1_arch"].settings["annotation_scale"] == "1:50"
|
||||
assert (rows["iso_a3_pid"].paper, rows["iso_a3_pid"].layer_set) == ("ISO_A3", "pid")
|
||||
assert (rows["ansi_b_mech"].paper, rows["ansi_b_mech"].dimstyle) == ("ANSI_B", "ANSI")
|
||||
assert (rows["ansi_d_arch"].paper, rows["ansi_d_arch"].scale) == ("ANSI_D", "fit")
|
||||
assert [spec.title_block for spec in TEMPLATE_CATALOG.values()] == [
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
]
|
||||
assert all(spec.orientation == "landscape" for spec in TEMPLATE_CATALOG.values())
|
||||
assert {spec.textstyle for spec in TEMPLATE_CATALOG.values()} == {"ISOCP", "ROMANS"}
|
||||
|
||||
|
||||
def test_template_rows_report_files():
|
||||
rows = template_rows()
|
||||
assert [row["name"] for row in rows] == list(NAMES)
|
||||
for row in rows:
|
||||
assert set(row["files"]) == {"dxf", "dwt"}
|
||||
assert row["files"]["dxf"]["path"].endswith(f"{row['name']}.dxf")
|
||||
assert isinstance(row["files"]["dxf"]["present"], bool)
|
||||
|
||||
|
||||
def test_resolve_template_bundled_path_and_unknown(tmp_path):
|
||||
path, source = resolve_template("iso_a3_mech", "ezdxf")
|
||||
assert source == "bundled" and path == str(TEMPLATES_DIR / "iso_a3_mech.dxf")
|
||||
path, source = resolve_template("ISO_A3_MECH", "ezdxf")
|
||||
assert source == "bundled"
|
||||
# COM prefers the .dwt twin and says when it fell back to the DXF
|
||||
path, source = resolve_template("iso_a3_mech", "com")
|
||||
assert source in ("bundled", "bundled_dxf")
|
||||
assert path.endswith(".dwt" if source == "bundled" else ".dxf")
|
||||
explicit = tmp_path / "mine.dwt"
|
||||
assert resolve_template(str(explicit), "ezdxf") == (str(explicit), "path")
|
||||
assert resolve_template("sheet.dxf", "com") == ("sheet.dxf", "path")
|
||||
with pytest.raises(
|
||||
ValueError, match="iso_a3_mech, iso_a1_arch, iso_a3_pid, ansi_b_mech, ansi_d_arch"
|
||||
):
|
||||
resolve_template("iso_a2_mech", "ezdxf")
|
||||
with pytest.raises(ValueError, match="template"):
|
||||
resolve_template("", "ezdxf")
|
||||
|
||||
|
||||
# ── the build ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_build_is_deterministic_run_to_run(tmp_path):
|
||||
build = _build_module()
|
||||
first = await build.build_all(tmp_path / "one")
|
||||
second = await build.build_all(tmp_path / "two")
|
||||
assert [row["name"] for row in first] == list(NAMES)
|
||||
for name in NAMES:
|
||||
a = build.normalised_lines(tmp_path / "one" / f"{name}.dxf")
|
||||
b = build.normalised_lines(tmp_path / "two" / f"{name}.dxf")
|
||||
assert a == b, f"{name} differs between two builds"
|
||||
assert all(row["page_setup"]["ok"] for row in first)
|
||||
assert all(row["layers"] for row in first)
|
||||
assert second # the second build is only there to be compared with the first
|
||||
|
||||
|
||||
async def test_committed_templates_match_a_fresh_build(tmp_path):
|
||||
"""The spec's reproducibility claim — the same comparison the script's --check runs."""
|
||||
build = _build_module()
|
||||
missing = [name for name in NAMES if not (TEMPLATES_DIR / f"{name}.dxf").is_file()]
|
||||
assert not missing, f"bundled templates missing: {missing} — run scripts/build_templates.py"
|
||||
await build.build_all(tmp_path)
|
||||
drifted = build.drifted_names(tmp_path)
|
||||
assert drifted == [], f"templates drifted from the script: {drifted}"
|
||||
|
||||
|
||||
# ── drawing_new(template=) ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_drawing_new_from_a_bundled_template(backend):
|
||||
result = await server.drawing_new(template="iso_a3_mech", ctx=_Ctx(backend))
|
||||
assert result["ok"] is True
|
||||
assert result["template"] == {
|
||||
"name": "iso_a3_mech",
|
||||
"path": str(TEMPLATES_DIR / "iso_a3_mech.dxf"),
|
||||
"source": "bundled",
|
||||
}
|
||||
layers = {layer.name for layer in await backend.layer_list()}
|
||||
assert {"GEOMETRY", "DIM", "HIDDEN", "CENTER", "TITLEBLOCK"} <= layers
|
||||
row = (await backend.page_setup_list("A3"))[0]
|
||||
assert row["paper"] == "ISO_A3" and row["orientation"] == "landscape"
|
||||
assert row["plot_style"] == "monochrome.ctb" and row["scale"] == "1:1"
|
||||
sheet = backend._doc.layouts.get("A3")
|
||||
assert any(e.dxftype() == "LWPOLYLINE" for e in sheet), "the ISO 5457 frame is on the sheet"
|
||||
assert float(await backend.system_get_variable("DIMTXT")) == 2.5
|
||||
if hasattr(backend, "dimstyle_create"): # after group S merges
|
||||
assert await backend.system_get_variable("DIMSTYLE") == "ISO-25"
|
||||
|
||||
|
||||
async def test_drawing_new_from_the_pid_and_ansi_templates(backend):
|
||||
await server.drawing_new(template="iso_a3_pid", ctx=_Ctx(backend))
|
||||
assert "PROCESS-PIPING-MAIN" in {layer.name for layer in await backend.layer_list()}
|
||||
await server.drawing_new(template="ansi_d_arch", ctx=_Ctx(backend))
|
||||
assert "M-GEOMET-E-N" in {layer.name for layer in await backend.layer_list()}
|
||||
row = (await backend.page_setup_list("D"))[0]
|
||||
assert row["paper"] == "ANSI_D" and row["size_mm"] == [864.0, 559.0] and row["scale"] == "fit"
|
||||
assert (await backend.drawing_settings())["settings"]["decimal_separator"] == "."
|
||||
|
||||
|
||||
#: Spec §4.1, the eight variables `_with_header_dimvars` does not fold — the
|
||||
#: ones a header-only write silently loses on the headless renderer.
|
||||
_PLACEMENT = ("dimtad", "dimtih", "dimtoh", "dimexo", "dimexe", "dimgap", "dimlunit", "dimlwd")
|
||||
|
||||
|
||||
async def _effective_dimvars(backend) -> dict:
|
||||
"""What the next dimension is really rendered with, read off the entity."""
|
||||
from ezdxf.entities import DimStyleOverride
|
||||
|
||||
info = await backend.dimension_linear(0, 0, 100, 0, 50, 20)
|
||||
override = DimStyleOverride(backend._doc.entitydb[info.handle])
|
||||
return {name: override.get(name) for name in _PLACEMENT + ("dimtxt", "dimasz")}
|
||||
|
||||
|
||||
async def test_templates_dimension_with_their_own_preset_headlessly(backend):
|
||||
"""The build's fallback lands on the ``Standard`` dimstyle, not only the header.
|
||||
|
||||
Measured before the fix: ``ansi_b_mech`` reported DIMTAD 0 / DIMTIH 1 from
|
||||
``system_get_variable`` while the dimension it drew used dimtad=1 / dimtih=0
|
||||
(the ISO values on ``Standard``), so the same template dimensioned
|
||||
differently per engine. The header and the style must agree.
|
||||
"""
|
||||
await server.drawing_new(template="ansi_b_mech", ctx=_Ctx(backend))
|
||||
ansi = await _effective_dimvars(backend)
|
||||
assert ansi == {
|
||||
"dimtad": 0,
|
||||
"dimtih": 1,
|
||||
"dimtoh": 1,
|
||||
"dimexo": 1.5,
|
||||
"dimexe": 1.5,
|
||||
"dimgap": 1.0,
|
||||
"dimlunit": 2,
|
||||
"dimlwd": -2,
|
||||
"dimtxt": 3.0,
|
||||
"dimasz": 3.0,
|
||||
}
|
||||
for name, value in ansi.items():
|
||||
header = await backend.system_get_variable(name.upper())
|
||||
assert float(header) == float(value), f"{name}: header {header} != style {value}"
|
||||
|
||||
await server.drawing_new(template="iso_a3_mech", ctx=_Ctx(backend))
|
||||
iso = await _effective_dimvars(backend)
|
||||
assert (iso["dimtad"], iso["dimtih"], iso["dimtoh"]) == (1, 0, 0)
|
||||
assert (iso["dimexo"], iso["dimexe"], iso["dimgap"]) == (0.625, 1.25, 0.625)
|
||||
assert (iso["dimtxt"], iso["dimasz"]) == (2.5, 2.5)
|
||||
|
||||
|
||||
async def test_drawing_new_unknown_template_is_refused_with_the_names(backend):
|
||||
await backend.entity_create_line(0, 0, 1, 1)
|
||||
before = (await backend.drawing_info()).entity_count
|
||||
with pytest.raises(ToolError, match="iso_a3_mech"):
|
||||
await server.drawing_new(template="iso_a2_mech", ctx=_Ctx(backend))
|
||||
assert (await backend.drawing_info()).entity_count == before, "the refusal replaced nothing"
|
||||
|
||||
|
||||
async def test_drawing_new_missing_template_path_is_refused(backend, tmp_path):
|
||||
with pytest.raises(ToolError, match="not found"):
|
||||
await server.drawing_new(template=str(tmp_path / "gone.dxf"), ctx=_Ctx(backend))
|
||||
|
||||
|
||||
async def test_drawing_new_from_an_explicit_dxf_path_reports_source_path(backend, tmp_path):
|
||||
await backend.layer_create("MINE", color=3)
|
||||
saved = tmp_path / "mine.dxf"
|
||||
await backend.drawing_save_as(str(saved), "dxf")
|
||||
result = await server.drawing_new(template=str(saved), ctx=_Ctx(backend))
|
||||
assert result["template"]["source"] == "path"
|
||||
assert result["template"]["path"] == str(saved.resolve())
|
||||
assert "MINE" in {layer.name for layer in await backend.layer_list()}
|
||||
|
||||
|
||||
async def test_drawing_template_list_tool(backend):
|
||||
result = await server.drawing_template_list(ctx=_Ctx(backend))
|
||||
assert result["ok"] and result["count"] == 5
|
||||
assert [row["name"] for row in result["templates"]] == list(NAMES)
|
||||
assert result["engine"] == "ezdxf"
|
||||
|
||||
|
||||
# ── COM: only a real .dwt reaches Documents.Add ─────────────────────────────
|
||||
|
||||
|
||||
class _FakeDocuments:
|
||||
"""Records the ActiveX calls; ``Add`` on a non-``.dwt`` answers blank, as
|
||||
AutoCAD 2026 does (measured: ``Add(<dxf>)`` == ``Add()`` == ``Add(<missing.dwt>)``)."""
|
||||
|
||||
def __init__(self, dwt_bytes: bytes = b"AC1032\x00template"):
|
||||
self.calls: list[tuple] = []
|
||||
self.dwt_bytes = dwt_bytes
|
||||
self.open_docs: list[_FakeDoc] = []
|
||||
|
||||
def Add(self, template=None):
|
||||
self.calls.append(("Add", template))
|
||||
real = template is not None and str(template).lower().endswith(".dwt")
|
||||
real = real and Path(template).is_file() and Path(template).read_bytes()[:6] == b"AC1032"
|
||||
return _FakeDoc(self, "Drawing1.dwg", layers=["0", "GEOMETRY"] if real else ["0"])
|
||||
|
||||
def Open(self, path, read_only=False, password=None):
|
||||
self.calls.append(("Open", path, read_only))
|
||||
doc = _FakeDoc(self, Path(path).name, layers=["0", "GEOMETRY"])
|
||||
self.open_docs.append(doc)
|
||||
return doc
|
||||
|
||||
|
||||
class _FakeDoc:
|
||||
def __init__(self, documents, name, layers):
|
||||
self._documents = documents
|
||||
self.Name = name
|
||||
self.layers = layers
|
||||
self.closed = None
|
||||
|
||||
def SaveAs(self, path, fmt=None):
|
||||
self._documents.calls.append(("SaveAs", path, fmt))
|
||||
if fmt != 66:
|
||||
raise RuntimeError(f"fake AutoCAD 2026 refuses SaveAs format {fmt}")
|
||||
Path(path).write_bytes(self._documents.dwt_bytes)
|
||||
|
||||
def Close(self, save=True):
|
||||
self._documents.calls.append(("Close", self.Name, save))
|
||||
self.closed = save
|
||||
self._documents.open_docs.remove(self)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_new(monkeypatch, tmp_path):
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
from backends import com_backend as module
|
||||
|
||||
documents = _FakeDocuments()
|
||||
app = types.SimpleNamespace(Documents=documents)
|
||||
monkeypatch.setattr(module, "_acad_app", lambda: app)
|
||||
monkeypatch.setattr(module, "_template_cache_dir", lambda: tmp_path / "cache")
|
||||
monkeypatch.setattr(module, "_COM_ERROR", (RuntimeError,))
|
||||
backend = module.ComBackend()
|
||||
|
||||
async def _run_inline(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def _no_state():
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(backend, "_run", _run_inline)
|
||||
monkeypatch.setattr(backend, "_ensure_document_state", _no_state)
|
||||
return backend, documents
|
||||
|
||||
|
||||
async def test_com_drawing_new_converts_a_dxf_into_a_real_dwt_once(com_new, tmp_path):
|
||||
backend, documents = com_new
|
||||
dxf = TEMPLATES_DIR / "iso_a3_pid.dxf"
|
||||
result = await backend.drawing_new(str(dxf))
|
||||
assert result["ok"] and result["name"] == "Drawing1.dwg"
|
||||
dwt = Path(result["template_dwt"]["path"])
|
||||
assert result["template_dwt"]["cached"] is False
|
||||
assert dwt.suffix == ".dwt" and dwt.is_file() and dwt.parent == tmp_path / "cache"
|
||||
assert documents.calls == [
|
||||
("Open", str(dxf), True),
|
||||
("SaveAs", str(dwt), 66),
|
||||
("Close", "iso_a3_pid.dxf", False),
|
||||
("Add", str(dwt)),
|
||||
], "open read-only, save as ac2018_Template, close without saving, then Add the .dwt"
|
||||
assert documents.open_docs == [], "the conversion document never stays open"
|
||||
|
||||
documents.calls.clear()
|
||||
again = await backend.drawing_new(str(dxf))
|
||||
assert again["template_dwt"] == {"path": str(dwt), "cached": True}
|
||||
assert documents.calls == [("Add", str(dwt))], "the second call reuses the cached .dwt"
|
||||
|
||||
|
||||
async def test_com_drawing_new_hands_a_dwt_straight_to_add_and_refuses_a_missing_file(
|
||||
com_new, tmp_path
|
||||
):
|
||||
backend, documents = com_new
|
||||
mine = tmp_path / "mine.dwt"
|
||||
mine.write_bytes(documents.dwt_bytes)
|
||||
result = await backend.drawing_new(str(mine))
|
||||
assert result == {"ok": True, "name": "Drawing1.dwg"}
|
||||
assert documents.calls == [("Add", str(mine))]
|
||||
|
||||
documents.calls.clear()
|
||||
with pytest.raises(FileNotFoundError, match="not found"):
|
||||
await backend.drawing_new(str(tmp_path / "gone.dwt"))
|
||||
assert documents.calls == [], "Documents.Add(<missing>) would silently create the default"
|
||||
|
||||
|
||||
async def test_com_drawing_new_falls_back_to_an_older_template_format(com_new, monkeypatch):
|
||||
backend, documents = com_new
|
||||
accepted = []
|
||||
|
||||
def _save_as(self, path, fmt=None):
|
||||
documents.calls.append(("SaveAs", path, fmt))
|
||||
if fmt == 66:
|
||||
raise RuntimeError("older seat: unknown SaveAs type")
|
||||
accepted.append(fmt)
|
||||
Path(path).write_bytes(documents.dwt_bytes)
|
||||
|
||||
monkeypatch.setattr(_FakeDoc, "SaveAs", _save_as)
|
||||
result = await backend.drawing_new(str(TEMPLATES_DIR / "ansi_b_mech.dxf"))
|
||||
assert result["ok"] and accepted == [62]
|
||||
assert documents.open_docs == []
|
||||
|
||||
|
||||
async def test_com_drawing_new_tool_reports_the_converted_dwt(com_new):
|
||||
backend, documents = com_new
|
||||
result = await server.drawing_new(template="iso_a3_pid", bootstrap=False, ctx=_Ctx(backend))
|
||||
assert result["ok"] is True
|
||||
assert "template_dwt" not in result
|
||||
template = result["template"]
|
||||
assert template["name"] == "iso_a3_pid"
|
||||
assert template["source"] in ("bundled", "bundled_dxf")
|
||||
if template["source"] == "bundled_dxf": # no .dwt twin committed: the live engine built one
|
||||
assert template["path"] == str(TEMPLATES_DIR / "iso_a3_pid.dxf")
|
||||
assert template["dwt"].endswith(".dwt") and template["dwt_cached"] is False
|
||||
assert documents.calls[-1] == ("Add", template["dwt"])
|
||||
else:
|
||||
assert "dwt" not in template and documents.calls == [("Add", template["path"])]
|
||||
|
||||
|
||||
# ── drawing_template_save ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_template_save_dxf_headless_round_trips(backend, tmp_path):
|
||||
await backend.layer_create("MINE", color=3)
|
||||
out = tmp_path / "mine_template.dxf"
|
||||
result = await backend.drawing_template_save(str(out), name="Mine", description="my sheet")
|
||||
assert result == {
|
||||
"ok": True,
|
||||
"path": str(out),
|
||||
"format": "dxf",
|
||||
"backend": "ezdxf",
|
||||
"name": "Mine",
|
||||
"description_written": False,
|
||||
"description_note": (
|
||||
"DXF has no template description field; it is a DWG/DWT summary property "
|
||||
"(capability 'dwgprops')"
|
||||
),
|
||||
}
|
||||
assert out.is_file()
|
||||
await server.drawing_new(template=str(out), ctx=_Ctx(backend))
|
||||
assert "MINE" in {layer.name for layer in await backend.layer_list()}
|
||||
|
||||
|
||||
async def test_template_save_dwt_headless_is_refused_with_the_capability(backend, tmp_path):
|
||||
out = tmp_path / "mine.dwt"
|
||||
with pytest.raises(UnsupportedCapabilityError) as excinfo:
|
||||
await backend.drawing_template_save(str(out))
|
||||
assert excinfo.value.capability == "dwt_write"
|
||||
message = str(excinfo.value)
|
||||
assert ".dxf" in message and "com" in message.lower()
|
||||
assert not out.exists()
|
||||
|
||||
|
||||
async def test_template_save_refuses_other_suffixes(backend, tmp_path):
|
||||
with pytest.raises(ValueError, match=r"\.dxf"):
|
||||
await backend.drawing_template_save(str(tmp_path / "mine.txt"))
|
||||
|
||||
|
||||
async def test_template_save_tool_validates_the_path(backend, tmp_path):
|
||||
with pytest.raises(ToolError):
|
||||
await server.drawing_template_save("../../outside.dxf", ctx=_Ctx(backend))
|
||||
result = await server.drawing_template_save(str(tmp_path / "t.dxf"), ctx=_Ctx(backend))
|
||||
assert result["ok"] and result["name"] == "t"
|
||||
|
||||
|
||||
class _SaveRecorder:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.SummaryInfo = types.SimpleNamespace()
|
||||
self.Name = "Drawing1.dwg"
|
||||
|
||||
def SaveAs(self, path, fmt=None):
|
||||
self.calls.append(("SaveAs", path, fmt))
|
||||
self.Name = Path(path).name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def com_backend(monkeypatch):
|
||||
pytest.importorskip("win32com.client", reason="pywin32 not installed")
|
||||
from backends import com_backend as module
|
||||
|
||||
document = _SaveRecorder()
|
||||
monkeypatch.setattr(module, "_acad_doc", lambda: document)
|
||||
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_template_save_dwt_uses_ac2018_template(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "mine.dwt")
|
||||
result = await backend.drawing_template_save(out, description="A3 mech")
|
||||
assert document.calls == [("SaveAs", out, 66)]
|
||||
assert document.SummaryInfo.Comments == "A3 mech"
|
||||
assert result == {
|
||||
"ok": True,
|
||||
"path": out,
|
||||
"format": "dwt",
|
||||
"backend": "com",
|
||||
"name": "mine",
|
||||
"description_written": True,
|
||||
"document_rebound": True,
|
||||
}
|
||||
|
||||
|
||||
async def test_com_template_save_dxf_uses_the_dxf_code(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "mine.dxf")
|
||||
result = await backend.drawing_template_save(out)
|
||||
assert document.calls == [("SaveAs", out, 61)]
|
||||
assert result["format"] == "dxf" and result["description_written"] is False
|
||||
|
||||
|
||||
async def test_com_save_as_dwt_no_longer_writes_an_r13_dxf(com_backend, tmp_path):
|
||||
"""drawing_save_as mapped 'dwt' to 5, which is AcSaveAsType.acR13_dxf."""
|
||||
backend, document = com_backend
|
||||
out = str(tmp_path / "x.dwt")
|
||||
await backend.drawing_save_as(out, "dwt")
|
||||
assert document.calls == [("SaveAs", out, 66)]
|
||||
|
||||
|
||||
async def test_com_template_save_refuses_other_suffixes_before_saving(com_backend, tmp_path):
|
||||
backend, document = com_backend
|
||||
with pytest.raises(ValueError, match=r"\.dwt"):
|
||||
await backend.drawing_template_save(str(tmp_path / "x.dwg"))
|
||||
assert document.calls == []
|
||||
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
|
||||
|
||||
|
||||
async def test_tool_group_sizes_are_unchanged():
|
||||
"""Frozen snapshot of the surface (161 tools, 20 groups).
|
||||
"""Frozen snapshot of the surface (172 tools, 20 groups).
|
||||
|
||||
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
|
||||
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
|
||||
@@ -528,8 +528,15 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
as a read-only tool) opened SECTION 20. `drawing` moved 11 -> 13 when
|
||||
v1.6's `drawing_properties_get` / `drawing_properties_set` (DWGPROPS on
|
||||
both engines) joined SECTION 20 — tagged `drawing` and `settings`, and
|
||||
`settings` is not a group tag, so they file under drawing. Every other
|
||||
number here has been unchanged since the snapshot was taken.
|
||||
`settings` is not a group tag, so they file under drawing.
|
||||
`layouts` moved 12 -> 16 when v1.6's SECTION 19 opened with
|
||||
`page_setup_list` / `page_setup_apply` / `plot_style_list` / `batch_plot`
|
||||
— all tagged `layout`, which `_GROUP_TAG_PRIORITY` ranks above
|
||||
`query`/`export`, so they file with the sheets they set up. `templates`
|
||||
moved 2 -> 3 when v1.6's `drawing_template_list` joined SECTION 19
|
||||
(tagged `template`, which ranks above `drawing`), then 3 -> 4 when
|
||||
`drawing_template_save` joined. Every other number
|
||||
here has been unchanged since the snapshot was taken.
|
||||
"""
|
||||
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
|
||||
assert sizes == {
|
||||
@@ -544,15 +551,15 @@ async def test_tool_group_sizes_are_unchanged():
|
||||
"entity_modification": 16,
|
||||
"entity_query": 9,
|
||||
"layers": 14,
|
||||
"layouts": 12,
|
||||
"layouts": 16,
|
||||
"pid": 9,
|
||||
"premium": 12,
|
||||
"solids": 5,
|
||||
"styles": 10,
|
||||
"system": 8,
|
||||
"templates": 2,
|
||||
"templates": 4,
|
||||
"transactions": 3,
|
||||
"validation": 1,
|
||||
"view": 4,
|
||||
}
|
||||
assert sum(sizes.values()) == 179
|
||||
assert sum(sizes.values()) == 185
|
||||
|
||||
Reference in New Issue
Block a user