-
-
Notifications
You must be signed in to change notification settings - Fork 901
Expand file tree
/
Copy pathtest_plot.py
More file actions
265 lines (221 loc) · 10.2 KB
/
test_plot.py
File metadata and controls
265 lines (221 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
from __future__ import annotations
import base64
import os
import subprocess
import sys
import tempfile
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import pytest
from ifcquery.plot import _highlight_css_from_ids, plot
try:
import ifcopenshell.draw # noqa: F401
HAS_DRAW = True
except ImportError:
HAS_DRAW = False
try:
import cairosvg # noqa: F401
HAS_CAIROSVG = True
except ImportError:
HAS_CAIROSVG = False
pytestmark = pytest.mark.skipif(not HAS_DRAW, reason="ifcopenshell.draw not available")
SVG_MAGIC = b"<?xml"
PNG_MAGIC = b"\x89PNG"
@pytest.fixture
def model_with_annotations():
"""IFC4 model with walls and explicit 2D annotation geometry (Plan/PLAN_VIEW context)."""
f = ifcopenshell.api.project.create_file()
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
ifcopenshell.api.unit.assign_unit(f)
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
storey.Elevation = 0.0 # required for setSectionHeightsFromStoreys() to create a cut plane
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
body = ifcopenshell.api.context.add_context(
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
)
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
return f, wall
@pytest.fixture
def model_no_plan(model_with_annotations):
"""Model whose SVG output will be empty (wall geometry only, no plan annotation group)."""
return model_with_annotations
class TestHighlightCSS:
def test_css_for_valid_element(self, model_with_annotations):
model, wall = model_with_annotations
css = _highlight_css_from_ids(model, [wall.id()])
assert wall.GlobalId in css
assert "opacity: 0.10" in css
assert "opacity: 1.0" in css
assert "#d00" in css
def test_css_empty_for_no_ids(self, model_with_annotations):
model, _ = model_with_annotations
css = _highlight_css_from_ids(model, [])
assert css == ""
def test_css_skips_unknown_ids(self, model_with_annotations):
model, _ = model_with_annotations
css = _highlight_css_from_ids(model, [999999])
assert css == ""
class TestPlotSVG:
def test_returns_svg_bytes(self, model_with_annotations):
model, _ = model_with_annotations
result = plot(model, output_format="svg")
assert isinstance(result, bytes)
assert result[:5] == SVG_MAGIC
def test_svg_contains_xml(self, model_with_annotations):
model, _ = model_with_annotations
result = plot(model, output_format="svg")
assert b"<svg" in result
def test_invalid_format_raises(self, model_with_annotations):
model, _ = model_with_annotations
with pytest.raises(ValueError, match="output_format"):
plot(model, output_format="xyz")
def test_invalid_view_raises(self, model_with_annotations):
model, _ = model_with_annotations
with pytest.raises(ValueError, match="view"):
plot(model, output_format="svg", view="bogus")
def test_selector_no_match_raises(self, model_with_annotations):
model, _ = model_with_annotations
with pytest.raises(ValueError, match="matched no elements"):
plot(model, output_format="svg", selector="IfcDoor")
def test_selector_filters_elements(self, model_with_annotations):
model, _ = model_with_annotations
result = plot(model, output_format="svg", selector="IfcWall")
assert isinstance(result, bytes)
assert b"<svg" in result
class TestPlotEmptySVG:
"""When draw produces no <g> elements, PNG/base64 should raise a clear error."""
def test_empty_drawing_png_raises(self, model_no_plan):
"""PNG format raises ValueError (not silently returns None) for empty drawings."""
model, _ = model_no_plan
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if not has_groups:
pytest.raises(ValueError, plot, model, output_format="png")
else:
pytest.skip("Model produced non-empty SVG — empty path not triggered")
def test_empty_drawing_base64_raises(self, model_no_plan):
"""base64 format raises ValueError (not silently returns None) for empty drawings."""
model, _ = model_no_plan
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if not has_groups:
pytest.raises(ValueError, plot, model, output_format="base64")
else:
pytest.skip("Model produced non-empty SVG — empty path not triggered")
@pytest.mark.skipif(not HAS_CAIROSVG, reason="cairosvg not installed")
class TestPlotPNG:
"""PNG and base64 require cairosvg."""
def test_png_returns_bytes_or_raises_on_empty(self, model_with_annotations):
model, _ = model_with_annotations
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if has_groups:
result = plot(model, output_format="png")
assert isinstance(result, bytes)
assert result[:4] == PNG_MAGIC
else:
with pytest.raises(ValueError, match="No plan geometry"):
plot(model, output_format="png")
def test_base64_returns_dict(self, model_with_annotations):
model, _ = model_with_annotations
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if has_groups:
result = plot(model, output_format="base64")
assert isinstance(result, dict)
assert result["mime"] == "image/png"
assert "png_b64" in result
assert "width" in result
assert "height" in result
assert "view" in result
# Verify the base64 is valid PNG
decoded = base64.b64decode(result["png_b64"])
assert decoded[:4] == PNG_MAGIC
else:
with pytest.raises(ValueError, match="No plan geometry"):
plot(model, output_format="base64")
def test_base64_view_field_matches_requested(self, model_with_annotations):
model, _ = model_with_annotations
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if not has_groups:
pytest.skip("Model produces empty SVG")
result = plot(model, output_format="base64", view="floorplan")
assert result["view"] == "floorplan"
def test_png_custom_size(self, model_with_annotations):
model, _ = model_with_annotations
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if not has_groups:
pytest.skip("Model produces empty SVG")
result = plot(model, output_format="png", png_width=512, png_height=512)
assert isinstance(result, bytes)
assert result[:4] == PNG_MAGIC
class TestCLI:
@staticmethod
def _ifc_path(model):
f = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
model.write(f.name)
f.close()
return f.name
def test_plot_svg_writes_file(self, model_with_annotations):
model, _ = model_with_annotations
ifc_path = self._ifc_path(model)
out_path = ifc_path.replace(".ifc", "_out.svg")
try:
result = subprocess.run(
[sys.executable, "-m", "ifcquery", ifc_path, "plot", "--out-format", "svg", "-o", out_path],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
assert os.path.exists(out_path)
with open(out_path, "rb") as f:
assert f.read(5) == SVG_MAGIC
finally:
for path in (ifc_path, out_path):
try:
os.unlink(path)
except OSError:
pass
@pytest.mark.skipif(not HAS_CAIROSVG, reason="cairosvg not installed")
def test_plot_base64_prints_json(self, model_with_annotations):
"""base64 format prints JSON to stdout instead of writing a file."""
model, _ = model_with_annotations
ifc_path = self._ifc_path(model)
try:
# First check if the model would produce geometry
svg = plot(model, output_format="svg")
has_groups = b"<g " in svg or b"<g>" in svg
if not has_groups:
pytest.skip("Model produces empty SVG — base64 would raise ValueError")
result = subprocess.run(
[sys.executable, "-m", "ifcquery", ifc_path, "plot", "--out-format", "base64"],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
# Output should be JSON (not an error) and contain base64 key
assert "png_b64" in result.stdout
finally:
try:
os.unlink(ifc_path)
except OSError:
pass