forked from ChelseaKR/outcome-receipts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_charts.py
More file actions
290 lines (222 loc) · 9.9 KB
/
Copy pathtest_charts.py
File metadata and controls
290 lines (222 loc) · 9.9 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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""Tests for charts drawn from grounded figures.
The load-bearing property is that a chart has no data path of its own: its bars
and points are the figures' values, and every number it renders is a figure
display, so the grounding gate verifies a chart the same way it verifies prose.
These tests pin that, plus the accessible data table and the SVG's image role.
"""
from __future__ import annotations
import re
import pytest
from outcome_receipts.charts import _scale_max, render_chart, render_charts
from outcome_receipts.grounding import ground
from outcome_receipts.models import ChartSpec, Figure, Receipt
def _figure(metric_id: str, value: float, display: str) -> Figure:
receipt = Receipt(
metric_id=metric_id,
value_sql="SELECT 1",
row_count=1,
slice_hash="x",
value=value,
unit="count",
computed_at="t",
)
return Figure(metric_id=metric_id, value=value, display=display, receipt=receipt)
FIGURES = [
_figure("permanent", 13.0, "13"),
_figure("temporary", 3.0, "3"),
_figure("unknown", 2.0, "2"),
]
BAR = ChartSpec(
chart_id="exits",
title="Exits by destination",
kind="bar",
metric_ids=("permanent", "temporary", "unknown"),
labels=("Permanent", "Temporary", "Unknown"),
)
def test_chart_points_are_the_figure_values_not_a_separate_path() -> None:
chart = render_chart(BAR, FIGURES)
assert [p.value for p in chart.points] == [13.0, 3.0, 2.0]
assert chart.displays == ("13", "3", "2")
def test_chart_numbers_all_ground_to_the_figures() -> None:
chart = render_chart(BAR, FIGURES)
result = ground(chart.claims_text, FIGURES)
assert result.ok
assert result.total == 3
def test_claims_text_excludes_svg_geometry() -> None:
# The SVG has pixel coordinates; the claims text the gate sees must not, or a
# presentational number would be mistaken for an ungrounded claim.
chart = render_chart(BAR, FIGURES)
assert "640" in chart.svg # canvas width is in the image
assert "640" not in chart.claims_text
def test_data_table_carries_the_grounded_numbers() -> None:
chart = render_chart(BAR, FIGURES)
assert "| Permanent | 13 |" in chart.data_table
assert "| Unknown | 2 |" in chart.data_table
def test_svg_is_an_accessible_image() -> None:
chart = render_chart(BAR, FIGURES)
assert 'role="img"' in chart.svg
assert "<title" in chart.svg
assert "<desc" in chart.svg
assert "Exits by destination" in chart.svg
def test_line_chart_renders_a_polyline() -> None:
spec = ChartSpec(
chart_id="trend", title="Trend", kind="line", metric_ids=("permanent", "temporary")
)
chart = render_chart(spec, FIGURES)
assert "<polyline" in chart.svg
assert chart.displays == ("13", "3")
def test_label_falls_back_to_metric_id() -> None:
spec = ChartSpec(chart_id="c", title="t", kind="bar", metric_ids=("permanent",))
chart = render_chart(spec, FIGURES)
assert chart.points[0].label == "permanent"
def test_unknown_metric_raises() -> None:
spec = ChartSpec(chart_id="c", title="t", kind="bar", metric_ids=("missing",))
with pytest.raises(KeyError, match="unknown metric"):
render_chart(spec, FIGURES)
def test_unknown_kind_raises() -> None:
spec = ChartSpec(chart_id="c", title="t", kind="pie", metric_ids=("permanent",))
with pytest.raises(ValueError, match="kind"):
render_chart(spec, FIGURES)
def test_render_charts_handles_several() -> None:
charts = render_charts([BAR, BAR], FIGURES)
assert len(charts) == 2
# --- Withheld cells: a bar height and a line slope are both claims. ---
#
# Merge-blocking (issue #78). A suppressed figure used to arrive here carrying
# ``value = 0.0``, so the bar drew flat on the axis baseline and the polyline
# ran straight through the floor and back up. The label said "[SUPPRESSED]" and
# the picture said "we housed nobody this quarter". These tests assert on the
# geometry, never on the label, because the label was always right.
_RECT = re.compile(r'<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)"([^>]*)')
_POLYLINE = re.compile(r'<polyline[^>]*points="([^"]+)"')
def _bars(svg: str) -> list[tuple[str, str, str, str, str]]:
"""Every plotted rectangle except the white canvas background."""
return [match for match in _RECT.findall(svg) if "#ffffff" not in match[4]]
def _withheld_figure(metric_id: str) -> Figure:
"""A figure in the state ``suppress_figures`` leaves a withheld cell in."""
from outcome_receipts.suppression import suppress_figures
publishable, result = suppress_figures([_figure(metric_id, 4.0, "4")])
assert result.suppressed == (metric_id,)
return publishable[0]
def _mixed_figures() -> list[Figure]:
"""One healthy value, one genuine zero, one withheld cell."""
return [
_figure("visible", 40.0, "40"),
_figure("true_zero", 0.0, "0"),
_withheld_figure("withheld"),
]
def test_a_withheld_bar_and_a_true_zero_bar_are_not_the_same_shape() -> None:
"""The exact comparison from the issue: identical geometry, before."""
spec = ChartSpec(
chart_id="mixed",
title="Mixed",
kind="bar",
metric_ids=("visible", "true_zero", "withheld"),
labels=("Visible", "Zero", "Withheld"),
)
svg = render_chart(spec, _mixed_figures()).svg
_visible, zero, withheld = _bars(svg)
assert (zero[1], zero[3]) != (withheld[1], withheld[3])
# The true zero keeps the old, correct rendering: nothing above the axis.
assert float(zero[3]) == 0.0
# The withheld slot occupies the full plot height, so it cannot be read as
# a small value either.
assert float(withheld[3]) > 0.0
assert float(withheld[1]) < float(zero[1])
# And it is visibly not a bar: not the data colour, and outlined dashed.
assert "#2b6cb0" not in withheld[4]
assert "stroke-dasharray" in withheld[4]
assert "url(#mixed-withheld)" in withheld[4]
def test_a_withheld_bar_is_announced_not_only_drawn() -> None:
spec = ChartSpec(
chart_id="mixed",
title="Mixed",
kind="bar",
metric_ids=("visible", "withheld"),
labels=("Visible", "Withheld"),
)
svg = render_chart(spec, _mixed_figures()).svg
assert "withheld under the small-cell suppression policy" in svg
assert "not a value of zero" in svg
assert "<desc" in svg and "1 category is withheld" in svg
def test_a_line_chart_does_not_interpolate_through_a_withheld_point() -> None:
"""40, withheld, 36. The old chart drew a collapse and a recovery."""
figures = [
_figure("q1", 40.0, "40"),
_withheld_figure("q2"),
_figure("q3", 36.0, "36"),
]
spec = ChartSpec(
chart_id="trend",
title="Trend",
kind="line",
metric_ids=("q1", "q2", "q3"),
labels=("Q1", "Q2", "Q3"),
)
svg = render_chart(spec, figures).svg
# No segment may span the gap. With a withheld point between the only two
# drawable ones, that leaves no polyline at all.
assert _POLYLINE.findall(svg) == []
# And no point is plotted on the axis floor at the withheld x position.
assert "<circle" in svg # the two real points are still drawn
assert 'cy="296.0"' not in svg
def test_a_line_chart_still_joins_the_points_on_either_side_of_a_gap() -> None:
"""The break must be exactly at the gap, not a refusal to draw anything."""
figures = [
_figure("q1", 40.0, "40"),
_figure("q2", 38.0, "38"),
_withheld_figure("q3"),
_figure("q4", 30.0, "30"),
_figure("q5", 36.0, "36"),
]
spec = ChartSpec(
chart_id="trend",
title="Trend",
kind="line",
metric_ids=("q1", "q2", "q3", "q4", "q5"),
)
svg = render_chart(spec, figures).svg
segments = _POLYLINE.findall(svg)
assert len(segments) == 2
assert all(len(segment.split()) == 2 for segment in segments)
# The withheld x (the midpoint of the plot) appears in neither segment.
assert not any("332.0," in segment for segment in segments)
def test_a_withheld_cell_does_not_participate_in_the_axis_scale() -> None:
"""A hidden cell must take no part in scaling the bars that are drawn.
Note this was latent rather than observable: `_scale_max` is a `max`, and a
withheld cell arriving as `0.0` could only have raised the maximum if every
real value were already at or below zero, which cannot change the clamped
result either. The test asserts the property directly, both on the scale
function and end to end, so it stays true if the scaling rule ever becomes
something other than a maximum.
"""
figures = _mixed_figures()
points = render_chart(
ChartSpec(
chart_id="c", title="t", kind="bar", metric_ids=("visible", "true_zero", "withheld")
),
figures,
).points
drawable = tuple(point for point in points if not point.withheld)
assert _scale_max(points) == _scale_max(drawable)
with_withheld = ChartSpec(
chart_id="c",
title="t",
kind="bar",
metric_ids=("visible", "withheld"),
)
without = ChartSpec(chart_id="c", title="t", kind="bar", metric_ids=("visible",))
drawn = _bars(render_chart(with_withheld, figures).svg)[0]
alone = _bars(render_chart(without, figures).svg)[0]
assert drawn[3] == alone[3]
# A chart of nothing but withheld cells must not divide by zero.
only = ChartSpec(chart_id="c", title="t", kind="bar", metric_ids=("withheld",))
assert render_chart(only, figures).svg
assert _scale_max(render_chart(only, figures).points) == 1.0
def test_a_withheld_point_carries_no_value_into_the_chart_data() -> None:
spec = ChartSpec(chart_id="c", title="t", kind="bar", metric_ids=("withheld",))
chart = render_chart(spec, _mixed_figures())
assert chart.points[0].value is None
assert chart.points[0].suppressed is True
assert chart.points[0].withheld is True
assert chart.data_table.endswith("| withheld | [SUPPRESSED] |")