"""Tests for output format generators (SVG, DXF, JSON).""" from __future__ import annotations import json import io import ezdxf import pytest from pipeline.postprocess import PathInfo, PostProcessResult, postprocess_svg from output.svg import generate_svg from output.json_output import generate_json from output.dxf import generate_dxf def _read_dxf(dxf_bytes: bytes) -> ezdxf.document.Drawing: """Read DXF bytes back into an ezdxf document (needs StringIO, not BytesIO).""" return ezdxf.read(io.StringIO(dxf_bytes.decode("utf-8"))) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- SIMPLE_SVG = ( '' '' "" ) # SVG with an outer CCW rectangle and an inner CW rectangle (island/hole) SVG_WITH_ISLAND = ( '' '' '' "" ) # SVG with an open path (no Z close) SVG_OPEN_PATH = ( '' '' "" ) def _make_result(svg: str = SIMPLE_SVG, epsilon: float = 0.1) -> PostProcessResult: """Run postprocess pipeline and return result for testing output generators.""" return postprocess_svg(svg, epsilon=epsilon) # --------------------------------------------------------------------------- # SVG output tests # --------------------------------------------------------------------------- class TestSVGOutput: def test_returns_string(self): result = _make_result() svg = generate_svg(result) assert isinstance(svg, str) def test_contains_svg_root(self): result = _make_result() svg = generate_svg(result) assert "" in svg def test_contains_path_element(self): result = _make_result() svg = generate_svg(result) assert " 0 path = parsed["paths"][0] assert "commands" in path assert "properties" in path def test_commands_have_correct_types(self): result = _make_result() parsed = json.loads(generate_json(result)) path = parsed["paths"][0] types = [cmd["type"] for cmd in path["commands"]] assert types[0] == "M" # starts with Move assert "L" in types # has line commands assert types[-1] == "Z" # closed path ends with Z def test_properties_fields(self): result = _make_result() parsed = json.loads(generate_json(result)) props = parsed["paths"][0]["properties"] assert "is_closed" in props assert "is_island" in props assert "node_count" in props assert "original_node_count" in props assert "area" in props def test_metadata_fields(self): result = _make_result() parsed = json.loads(generate_json(result)) meta = parsed["metadata"] assert "path_count" in meta assert "total_nodes" in meta assert "total_original_nodes" in meta assert "open_path_count" in meta assert "island_count" in meta def test_metadata_path_count_matches(self): result = _make_result() parsed = json.loads(generate_json(result)) assert parsed["metadata"]["path_count"] == len(parsed["paths"]) def test_island_flagged_in_properties(self): result = _make_result(SVG_WITH_ISLAND) parsed = json.loads(generate_json(result)) island_flags = [p["properties"]["is_island"] for p in parsed["paths"]] assert True in island_flags # at least one island detected def test_open_path_not_closed(self): result = _make_result(SVG_OPEN_PATH) parsed = json.loads(generate_json(result)) # Open path should not end with Z path = parsed["paths"][0] types = [cmd["type"] for cmd in path["commands"]] assert types[-1] != "Z" # --------------------------------------------------------------------------- # DXF output tests # --------------------------------------------------------------------------- class TestDXFOutput: def test_returns_bytes(self): result = _make_result() dxf = generate_dxf(result) assert isinstance(dxf, bytes) def test_valid_dxf_structure(self): result = _make_result() dxf_bytes = generate_dxf(result) # ezdxf can read it back doc = _read_dxf(dxf_bytes) assert doc.dxfversion >= "AC1015" def test_contains_lwpolyline_entities(self): result = _make_result() dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) assert len(polylines) > 0 def test_polyline_has_correct_point_count(self): result = _make_result() dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) # Simple square: 4 unique points (close flag handles the fifth) total_points = sum(len(list(pl.get_points())) for pl in polylines) assert total_points >= 4 def test_closed_path_produces_closed_polyline(self): result = _make_result() dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) # At least one closed polyline assert any(pl.is_closed for pl in polylines) def test_island_on_islands_layer(self): result = _make_result(SVG_WITH_ISLAND) dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) layers = [pl.dxf.layer for pl in polylines] assert "ISLANDS" in layers def test_outer_contour_on_default_layer(self): result = _make_result(SVG_WITH_ISLAND) dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) layers = [pl.dxf.layer for pl in polylines] assert "0" in layers def test_ac1015_version(self): result = _make_result() dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) assert doc.dxfversion == "AC1015" def test_dxf_header_present(self): result = _make_result() dxf_bytes = generate_dxf(result) # Raw bytes should contain DXF section markers text = dxf_bytes.decode("ascii", errors="replace") assert "HEADER" in text assert "ENTITIES" in text def test_open_path_produces_open_polyline(self): result = _make_result(SVG_OPEN_PATH) dxf_bytes = generate_dxf(result) doc = _read_dxf(dxf_bytes) msp = doc.modelspace() polylines = list(msp.query("LWPOLYLINE")) # Open path should produce non-closed polyline assert any(not pl.is_closed for pl in polylines) # --------------------------------------------------------------------------- # Integration: round-trip through all formats # --------------------------------------------------------------------------- class TestRoundTrip: def test_all_formats_from_same_input(self): """All three generators produce valid output from the same PostProcessResult.""" result = _make_result() svg = generate_svg(result) json_out = generate_json(result) dxf = generate_dxf(result) assert isinstance(svg, str) and " 0 def test_path_count_consistent_across_formats(self): """JSON and DXF should have the same number of paths as the result.""" result = _make_result(SVG_WITH_ISLAND) parsed_json = json.loads(generate_json(result)) doc = _read_dxf(generate_dxf(result)) msp = doc.modelspace() dxf_polylines = list(msp.query("LWPOLYLINE")) assert parsed_json["metadata"]["path_count"] == len(result.paths) assert len(dxf_polylines) == len(result.paths)