Audit-driven fixes across the dual backend, the premium workflow, and the engineering layer. 318 tests, ruff-clean. Quality gate (the moat): - drawing_finalize now runs the premium ISO-128 critique in addition to the 8-step validator; leftover construction, non-ISO lineweights, untrimmed corners, duplicates and dim overlap block the gate (strict_critique fails on warnings too). - dim_overlap critique no longer a no-op (dim reference point + bbox fallback); dim/construction layers resolve from the active layer set (iso13567 too). Stability / correctness: - view_screenshot & drawing_export_pdf render headless via Agg (no more worker-thread SIGSEGV). - gear tooth profile no longer self-overlaps at high tooth counts; section view no longer duplicates bore lines; validator keyway heuristic fixed. - ezdxf aligned/angular dim API, full-circle polar array duplicate already covered by Sprint 1; this bundle adds save-format-from-extension, entity_offset side selection, ARC length, properties parity (bounding_box / rotation / char_height), mtext rotation. COM robustness (mocked-COM tested): - CoUninitialize on teardown; transaction commit/rollback try/finally + UNDO via the CMDACTIVE guard; system_run_lisp guard + USERS1 value capture; set_variable type coercion; HWND via Application.HWND; BlockInfo.description. Security: - HTTP remote-bind guard fires on every launch path (incl. `fastmcp run`); StaticTokenVerifier wired into FastMCP; AutoLISP allowlist bypass-vector tests. Hygiene: linetype-on-demand in _apply_attrs, honest view_zoom returns, snap-on-arc sweep handling, point_tangent acos clamp, dynamic tool_groups / tool count, word-boundary SPUR/HELICAL, undo-stack locking. Deferred: R22 (undo/transaction stack isolation) lands with the I1 refiner (1.2), which depends on it.
108 lines
4.7 KiB
Python
108 lines
4.7 KiB
Python
"""Regression tests for the CI-verifiable (ezdxf / shared) Sprint-3 fixes.
|
|
|
|
* N2 — drawing_save_as derives the on-disk format from the path extension
|
|
* N3 — ARC carries a `length` property; entity_select_smart length_range selects it
|
|
* N5 — properties parity: ezdxf sets bounding_box; MTEXT carries char_height + rotation
|
|
* NEW-dimauto-baseline-rotation — baseline dims run parallel to a rotated baseline
|
|
* NEW-dimauto-ordinate-misnomer — ordinate dims are staggered, not stacked at one offset
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
class _Ctx:
|
|
def __init__(self, backend):
|
|
self.lifespan_context = {"backend": backend}
|
|
|
|
async def info(self, message):
|
|
pass
|
|
|
|
|
|
# ── N2 — format follows the file extension ──────────────────────────────────
|
|
|
|
async def test_save_as_derives_format_from_extension(backend):
|
|
import server
|
|
|
|
ctx = _Ctx(backend)
|
|
d = tempfile.mkdtemp()
|
|
res = await server.drawing_save_as(path=os.path.join(d, "part.dxf"), ctx=ctx)
|
|
assert res.get("format") == "dxf" # not the old "dwg" default
|
|
|
|
# ezdxf cannot write real DWG — must refuse rather than mislabel a .dwg file.
|
|
with pytest.raises(RuntimeError):
|
|
await server.drawing_save_as(path=os.path.join(d, "part.dwg"), ctx=ctx)
|
|
|
|
|
|
# ── N3 — ARC length + length_range selection ────────────────────────────────
|
|
|
|
async def test_arc_has_length_and_is_selectable(backend):
|
|
arc = await backend.entity_create_arc(0, 0, 10, 0, 90) # r=10, 90deg -> len ~15.708
|
|
info = await backend.entity_get(arc.handle)
|
|
assert info.properties.get("length") == pytest.approx(15.70796, rel=1e-4)
|
|
|
|
sel = await backend.entity_select_smart({"type": "ARC", "length_range": [15, 16]})
|
|
assert len(sel) == 1
|
|
# Out-of-range excludes it (was: all arcs silently rejected for lack of length).
|
|
assert await backend.entity_select_smart({"type": "ARC", "length_range": [1, 2]}) == []
|
|
|
|
|
|
# ── N5 — cross-backend property parity ──────────────────────────────────────
|
|
|
|
async def test_ezdxf_populates_bounding_box(backend):
|
|
line = await backend.entity_create_line(0, 0, 30, 40)
|
|
info = await backend.entity_get(line.handle)
|
|
bb = info.properties.get("bounding_box")
|
|
assert bb is not None and set(bb) == {"min", "max"} # same shape as COM
|
|
|
|
|
|
async def test_mtext_has_char_height_and_rotation(backend):
|
|
mt = await backend.entity_create_mtext("hello", 5, 5, height=2.5)
|
|
info = await backend.entity_get(mt.handle)
|
|
assert "char_height" in info.properties
|
|
assert "rotation" in info.properties
|
|
|
|
|
|
async def test_mtext_honors_caller_rotation(backend):
|
|
# NEW-mtext-1: entity_create_mtext now accepts a rotation that round-trips.
|
|
mt = await backend.entity_create_mtext("rot", 0, 0, width=50, height=2.5, rotation=30.0)
|
|
info = await backend.entity_get(mt.handle)
|
|
assert float(info.properties.get("rotation", 0.0)) == pytest.approx(30.0, abs=1e-6)
|
|
|
|
|
|
# ── NEW-dimauto-baseline-rotation ───────────────────────────────────────────
|
|
|
|
async def test_baseline_dim_runs_parallel_to_rotated_baseline(backend):
|
|
ln = await backend.entity_create_line(0, 0, 40, 40) # 45-degree baseline
|
|
dims = await backend.dimension_auto([ln.handle], style="baseline")
|
|
doc = backend._require_doc()
|
|
ent = doc.entitydb[dims[0].handle]
|
|
# Dimension line angle must match the baseline (45), not the old hardcoded 0.
|
|
assert ent.dxf.get("angle", 0.0) == pytest.approx(45.0, abs=1e-6)
|
|
assert ent.get_measurement() == pytest.approx(56.5685, rel=1e-3) # true len, not projection
|
|
|
|
|
|
# ── NEW-dimauto-ordinate-misnomer ───────────────────────────────────────────
|
|
|
|
async def test_ordinate_dims_are_staggered_not_overlapping(backend):
|
|
l1 = await backend.entity_create_line(0, 0, 20, 10)
|
|
l2 = await backend.entity_create_line(0, 0, 40, 30)
|
|
dims = await backend.dimension_auto([l1.handle, l2.handle], style="ordinate")
|
|
# Each segment emits an X and a Y dim; with per-feature staggering the
|
|
# X-dim reference Y-offsets differ between the two features.
|
|
doc = backend._require_doc()
|
|
y_offsets = []
|
|
for d in dims:
|
|
ent = doc.entitydb[d.handle]
|
|
dp = ent.dxf.get("defpoint", None)
|
|
if dp is not None:
|
|
y_offsets.append(round(float(dp[1]), 3))
|
|
# Not every dim collapses onto a single offset value.
|
|
assert len(set(y_offsets)) > 1
|