diff --git a/engineering/understand/snapshot.py b/engineering/understand/snapshot.py index 2cf93ab..76cab08 100644 --- a/engineering/understand/snapshot.py +++ b/engineering/understand/snapshot.py @@ -297,11 +297,16 @@ def records_from_doc(doc, *, source: str) -> Snapshot: def _read(path: str, *, source: str) -> Snapshot: + # Every parse or walk failure is one refusal. A truncated file makes ezdxf's + # tag reader raise StopIteration, which asyncio.to_thread cannot hand back to + # an awaiting tool - uncaught, the tool would never answer. try: - doc = ezdxf.readfile(path) - except (OSError, ezdxf.DXFError) as exc: - raise ValueError(f"{path} is not a readable DXF file: {exc}") from exc - return records_from_doc(doc, source=source) + return records_from_doc(ezdxf.readfile(path), source=source) + except MemoryError: + raise + except Exception as exc: # noqa: BLE001 - see above + reason = str(exc) or type(exc).__name__ + raise ValueError(f"{path} is not a readable DXF file: {reason}") from exc def read_snapshot(path: str) -> Snapshot: diff --git a/tests/test_understand_snapshot.py b/tests/test_understand_snapshot.py index a136b07..2150d62 100644 --- a/tests/test_understand_snapshot.py +++ b/tests/test_understand_snapshot.py @@ -8,6 +8,7 @@ behaves the way AutoCAD 2026 was measured to behave (the extension must be from __future__ import annotations +import asyncio import math from pathlib import Path @@ -173,6 +174,23 @@ def test_read_snapshot_refuses_missing_and_unreadable_files(tmp_path): assert "not a readable DXF" in str(excinfo.value) +def test_a_truncated_dxf_is_refused_not_left_hanging(tmp_path): + # An interrupted save: the header opens and the file ends. ezdxf's tag reader + # raises StopIteration here, which asyncio.to_thread cannot hand back to an + # awaiting tool - the call would never answer. It must be the same refusal + # as any other unreadable file, also through a worker thread. + half = tmp_path / "half.dxf" + half.write_text(" 0\nSECTION\n 2\nHEADER\n 9\n$ACADVER\n 1\nAC1015\n", encoding="utf-8") + with pytest.raises(ValueError, match="not a readable DXF"): + read_snapshot(str(half)) + + async def through_a_thread(): + return await asyncio.wait_for(asyncio.to_thread(read_snapshot, str(half)), timeout=10) + + with pytest.raises(ValueError, match="not a readable DXF"): + asyncio.run(through_a_thread()) + + # -- take_snapshot on the headless engine ---------------------------------------