dynamic-systems-testing-framework 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. dstf/__init__.py +41 -0
  2. dstf/__main__.py +10 -0
  3. dstf/authoring/__init__.py +13 -0
  4. dstf/authoring/builtin/__init__.py +32 -0
  5. dstf/authoring/builtin/accept.py +205 -0
  6. dstf/authoring/builtin/edit_criteria.py +479 -0
  7. dstf/authoring/builtin/edit_family.py +228 -0
  8. dstf/authoring/builtin/edit_on_plot.py +1354 -0
  9. dstf/authoring/builtin/edit_tree.py +1399 -0
  10. dstf/authoring/builtin/enqueue.py +144 -0
  11. dstf/authoring/capabilities.py +463 -0
  12. dstf/authoring/errors.py +49 -0
  13. dstf/authoring/family_rows.py +526 -0
  14. dstf/authoring/forms.py +631 -0
  15. dstf/authoring/layout.py +104 -0
  16. dstf/authoring/live_rows.py +337 -0
  17. dstf/authoring/live_runs.py +385 -0
  18. dstf/authoring/needle.py +127 -0
  19. dstf/authoring/picker_rows.py +92 -0
  20. dstf/authoring/queue_rows.py +354 -0
  21. dstf/authoring/session.py +3016 -0
  22. dstf/authoring/shell/__init__.py +9 -0
  23. dstf/authoring/shell/actions.py +702 -0
  24. dstf/authoring/shell/app.py +53 -0
  25. dstf/authoring/shell/drawer_controls.py +190 -0
  26. dstf/authoring/shell/family_fold.py +468 -0
  27. dstf/authoring/shell/figure_hook.py +249 -0
  28. dstf/authoring/shell/grips.js +1000 -0
  29. dstf/authoring/shell/header.py +783 -0
  30. dstf/authoring/shell/live_view.py +93 -0
  31. dstf/authoring/shell/picker.py +267 -0
  32. dstf/authoring/shell/placing.py +122 -0
  33. dstf/authoring/shell/plot_card.py +1171 -0
  34. dstf/authoring/shell/queue_panel.py +433 -0
  35. dstf/authoring/shell/rail.py +593 -0
  36. dstf/authoring/shell/shell.py +528 -0
  37. dstf/authoring/shell/stats_card.py +501 -0
  38. dstf/authoring/shell/strip.py +204 -0
  39. dstf/authoring/shell/style.py +721 -0
  40. dstf/authoring/shell/suite.py +277 -0
  41. dstf/authoring/shell/suite_grid.js +414 -0
  42. dstf/authoring/shell/suite_grid.py +373 -0
  43. dstf/authoring/shell/tab_theme.py +110 -0
  44. dstf/authoring/shell/tabstate.py +146 -0
  45. dstf/authoring/shell/test_list.py +179 -0
  46. dstf/authoring/shell/test_page.py +1404 -0
  47. dstf/authoring/shell/tree_card.py +1171 -0
  48. dstf/authoring/shell/variables_drawer.py +589 -0
  49. dstf/authoring/shell/widgets.py +188 -0
  50. dstf/authoring/situation.py +287 -0
  51. dstf/authoring/spec_document.py +215 -0
  52. dstf/authoring/spec_preview.py +83 -0
  53. dstf/authoring/suite_rows.py +99 -0
  54. dstf/authoring/tree_move.py +516 -0
  55. dstf/authoring/tree_pointer.py +33 -0
  56. dstf/authoring/tree_text.py +61 -0
  57. dstf/authoring/typed_number.js +64 -0
  58. dstf/authoring/typed_number.py +76 -0
  59. dstf/authoring/unscorable.py +141 -0
  60. dstf/authoring/variable_rows.py +216 -0
  61. dstf/backends/__init__.py +283 -0
  62. dstf/backends/base.py +1117 -0
  63. dstf/backends/common/__init__.py +1 -0
  64. dstf/backends/common/json_driver.py +484 -0
  65. dstf/backends/common/live_mat.py +369 -0
  66. dstf/backends/common/mat_reader.py +632 -0
  67. dstf/backends/common/proc_kill.py +238 -0
  68. dstf/backends/common/proc_output.py +20 -0
  69. dstf/backends/common/result_files.py +109 -0
  70. dstf/backends/common/result_variables.py +329 -0
  71. dstf/backends/common/statistics.py +39 -0
  72. dstf/backends/cross_backend.py +193 -0
  73. dstf/backends/dymola/__init__.py +5 -0
  74. dstf/backends/dymola/backend.py +776 -0
  75. dstf/backends/dymola/interface_loader.py +241 -0
  76. dstf/backends/dymola/log_parser.py +396 -0
  77. dstf/backends/dymola/persistent_backend.py +949 -0
  78. dstf/backends/fmpy/__init__.py +11 -0
  79. dstf/backends/fmpy/backend.py +443 -0
  80. dstf/backends/julia/__init__.py +27 -0
  81. dstf/backends/julia/backend.py +156 -0
  82. dstf/backends/julia/persistent_backend.py +541 -0
  83. dstf/backends/julia/run_persistent.jl +167 -0
  84. dstf/backends/julia/run_test.jl +146 -0
  85. dstf/backends/openmodelica/__init__.py +21 -0
  86. dstf/backends/openmodelica/backend.py +368 -0
  87. dstf/backends/openmodelica/init_xml.py +76 -0
  88. dstf/backends/openmodelica/log_parser.py +119 -0
  89. dstf/backends/openmodelica/mos_generator.py +228 -0
  90. dstf/backends/openmodelica/persistent_backend.py +1045 -0
  91. dstf/backends/openmodelica/session_loader.py +63 -0
  92. dstf/backends/persistent.py +1811 -0
  93. dstf/backends/pool.py +602 -0
  94. dstf/backends/progress.py +395 -0
  95. dstf/backends/python/__init__.py +11 -0
  96. dstf/backends/python/backend.py +141 -0
  97. dstf/backends/python/run_test.py +141 -0
  98. dstf/backends/run_metadata.py +103 -0
  99. dstf/backends/types.py +310 -0
  100. dstf/cli/__init__.py +12 -0
  101. dstf/cli/author_cmd.py +61 -0
  102. dstf/cli/baseline_cmds.py +341 -0
  103. dstf/cli/common.py +231 -0
  104. dstf/cli/diag_cmds.py +350 -0
  105. dstf/cli/main.py +561 -0
  106. dstf/cli/review.py +398 -0
  107. dstf/cli/run_cmds.py +589 -0
  108. dstf/cli/spec_cmds.py +123 -0
  109. dstf/comparison/__init__.py +13 -0
  110. dstf/comparison/algorithms.py +2127 -0
  111. dstf/comparison/builtins.py +291 -0
  112. dstf/comparison/comparator.py +1189 -0
  113. dstf/comparison/geometry.py +164 -0
  114. dstf/comparison/handles.py +1194 -0
  115. dstf/comparison/metric_tree.py +332 -0
  116. dstf/comparison/mode_schema.py +176 -0
  117. dstf/comparison/modes.py +788 -0
  118. dstf/comparison/registry.py +230 -0
  119. dstf/comparison/seeds.py +281 -0
  120. dstf/comparison/sides.py +158 -0
  121. dstf/comparison/snap.py +62 -0
  122. dstf/comparison/strict_keys.py +246 -0
  123. dstf/comparison/summaries.py +354 -0
  124. dstf/comparison/tree_eval.py +868 -0
  125. dstf/comparison/tree_spec.py +678 -0
  126. dstf/comparison/types.py +194 -0
  127. dstf/comparison/validator.py +232 -0
  128. dstf/config.py +815 -0
  129. dstf/discovery/__init__.py +0 -0
  130. dstf/discovery/json_recognizer.py +702 -0
  131. dstf/discovery/library_state.py +248 -0
  132. dstf/discovery/mo_parser.py +794 -0
  133. dstf/discovery/patch_apply.py +308 -0
  134. dstf/discovery/patch_envelope.py +63 -0
  135. dstf/discovery/plots.py +318 -0
  136. dstf/discovery/recognizer.py +103 -0
  137. dstf/discovery/scan_cache.py +154 -0
  138. dstf/discovery/spec_parser.py +538 -0
  139. dstf/discovery/spec_shape.py +122 -0
  140. dstf/discovery/spec_update.py +493 -0
  141. dstf/discovery/test_registry.py +488 -0
  142. dstf/discovery/unit_tests.py +95 -0
  143. dstf/figures/__init__.py +14 -0
  144. dstf/figures/assemble.py +48 -0
  145. dstf/figures/decimate.py +150 -0
  146. dstf/figures/families.py +289 -0
  147. dstf/figures/palette.py +170 -0
  148. dstf/figures/plotly.py +1413 -0
  149. dstf/figures/view.py +233 -0
  150. dstf/pipeline.py +435 -0
  151. dstf/plugins.py +104 -0
  152. dstf/reporting/__init__.py +0 -0
  153. dstf/reporting/console_report.py +187 -0
  154. dstf/reporting/context_builder.py +979 -0
  155. dstf/reporting/dashboard_render.py +534 -0
  156. dstf/reporting/junit_report.py +192 -0
  157. dstf/reporting/overlay_loader.py +708 -0
  158. dstf/reporting/report_suite.py +1059 -0
  159. dstf/reporting/rows.py +458 -0
  160. dstf/reporting/schema_export.py +413 -0
  161. dstf/reporting/stats_rows.py +321 -0
  162. dstf/reporting/status.py +388 -0
  163. dstf/reporting/templates/axes.js +61 -0
  164. dstf/reporting/templates/dashboard.html +284 -0
  165. dstf/reporting/templates/dashboard.js +604 -0
  166. dstf/reporting/templates/filters.js +100 -0
  167. dstf/reporting/templates/interactive.html +420 -0
  168. dstf/reporting/templates/report.js +116 -0
  169. dstf/reporting/templates/vendor/plotly.min.js +8 -0
  170. dstf/reporting/theme.py +324 -0
  171. dstf/reporting/tree_rows.py +73 -0
  172. dstf/reporting/verdicts.py +124 -0
  173. dstf/storage/__init__.py +0 -0
  174. dstf/storage/accept.py +216 -0
  175. dstf/storage/baseline.py +43 -0
  176. dstf/storage/batch_manifest.py +204 -0
  177. dstf/storage/ref_index.py +295 -0
  178. dstf/storage/reference_store.py +661 -0
  179. dstf/storage/roles.py +576 -0
  180. dstf/storage/run_stats.py +85 -0
  181. dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
  182. dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
  183. dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
  184. dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
  185. dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,324 @@
1
+ """The visual system's tokens (DESIGN.md "Colors", "Typography", "Themes"):
2
+ one table both tiers read. Rendered to CSS custom properties for the
3
+ dashboard, the per-test report and the tool's page, and mirrored into the
4
+ tool's Quasar palette. The figure layer keeps its literal hex: what a
5
+ figure takes from a theme is a table of its own, under the same names
6
+ (``figures/palette.py``).
7
+
8
+ A theme is a NAME of :data:`THEMES` (D148). It sets the neutrals, the
9
+ accent, the selection tint and the text on an accent fill. ``ROLES`` and
10
+ every status FILL are the same under every theme; a dark theme dims what a
11
+ status lays behind text (:func:`status_colours`).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping
17
+ from types import MappingProxyType
18
+
19
+ #: Corrected stack (DESIGN.md "Typography"): the shipped one ended in
20
+ #: `monospace` with no generic sans-serif.
21
+ FONT_STACK = '"Segoe UI", Roboto, system-ui, -apple-system, sans-serif'
22
+ MONO_STACK = 'ui-monospace, Consolas, "Cascadia Mono", monospace'
23
+
24
+ DEFAULT_THEME: Mapping[str, str] = MappingProxyType(
25
+ {
26
+ "page": "#fafafa",
27
+ "surface": "#ffffff",
28
+ "surface-hover": "#f5f5f5",
29
+ #: One step past surface-hover, for a control that is itself a
30
+ #: hoverable surface: a `th` rests on surface-hover, so its own
31
+ #: hover needs somewhere else to go.
32
+ "surface-active": "#ececec",
33
+ "border": "#dddddd",
34
+ "border-hairline": "#eeeeee",
35
+ "text": "#333333",
36
+ "text-secondary": "#666666",
37
+ "text-muted": "#888888",
38
+ "text-disabled": "#999999",
39
+ "accent": "#1976D2",
40
+ "accent-hover": "#1565C0",
41
+ "selection": "#e3f2fd",
42
+ "shadow": "rgba(0,0,0,0.1)",
43
+ #: Under a floating menu (the Suite table's column chooser), which
44
+ #: lies over the content it shades: a step past ``shadow``.
45
+ "shadow-menu": "rgba(0,0,0,0.15)",
46
+ #: Text on a FILLED accent (the primary button). A token of its
47
+ #: own: on a dark surface no one blue reads as link text and
48
+ #: carries white text at once (white on the dark accent is 2.2:1).
49
+ "on-accent": "#ffffff",
50
+ }
51
+ )
52
+
53
+ #: Neutral near-black, never blue-tinted (D148). The page is Quasar's own
54
+ #: dark page, so a Quasar piece nobody restyled sits on the same ground.
55
+ DARK_THEME: Mapping[str, str] = MappingProxyType(
56
+ {
57
+ "page": "#121212",
58
+ "surface": "#1e1e1e",
59
+ "surface-hover": "#2a2a2a",
60
+ "surface-active": "#353535",
61
+ "border": "#484848",
62
+ "border-hairline": "#303030",
63
+ "text": "#e4e4e4",
64
+ "text-secondary": "#b8b8b8",
65
+ "text-muted": "#9e9e9e",
66
+ "text-disabled": "#7d7d7d",
67
+ "accent": "#64b5f6",
68
+ "accent-hover": "#90caf9",
69
+ "selection": "#1d3a57",
70
+ "shadow": "rgba(0,0,0,0.5)",
71
+ "shadow-menu": "rgba(0,0,0,0.6)",
72
+ "on-accent": "#0b1a2b",
73
+ }
74
+ )
75
+
76
+ #: The bundled themes, by the name `testing.json`'s ``"theme"`` says. A
77
+ #: name only (brief §3.10): no theme file, no inline colours.
78
+ THEMES: Mapping[str, Mapping[str, str]] = MappingProxyType(
79
+ {"default": DEFAULT_THEME, "dark": DARK_THEME}
80
+ )
81
+
82
+ #: The themes that write light on dark: Quasar's dark mode, the browser's
83
+ #: ``color-scheme`` and the dimmed status parts follow this.
84
+ DARK_THEMES = frozenset({"dark"})
85
+
86
+
87
+ def theme_named(name: str) -> Mapping[str, str]:
88
+ """The token table of the bundled theme ``name``; any other name is a
89
+ ``ValueError`` that says the bundled ones."""
90
+ try:
91
+ return THEMES[name]
92
+ except KeyError:
93
+ raise ValueError(
94
+ f"unknown theme {name!r}: the bundled themes are {', '.join(THEMES)}"
95
+ ) from None
96
+
97
+
98
+ def is_dark(name: str) -> bool:
99
+ theme_named(name)
100
+ return name in DARK_THEMES
101
+
102
+
103
+ #: The neutral grey: a live row still queued, a test with no result (NOT
104
+ #: RUN, D145) and a row the tool has not scored yet (SCORING) — one token
105
+ #: under three classes, so a pill and a filter can still tell them apart.
106
+ _NEUTRAL: Mapping[str, str] = MappingProxyType(
107
+ {
108
+ "fill": "#888888",
109
+ "dark": "#555555",
110
+ "tint": "#f5f5f5",
111
+ "pill": "#f5f5f5",
112
+ "border": "#dddddd",
113
+ }
114
+ )
115
+
116
+ #: fill: the filled badge and bar segment; dark: text on a tint; tint: the
117
+ #: row background; pill: the tinted pill's background; border: its border.
118
+ STATUS: Mapping[str, Mapping[str, str]] = MappingProxyType(
119
+ {
120
+ "pass": MappingProxyType(
121
+ {
122
+ "fill": "#4CAF50",
123
+ "dark": "#2E7D32",
124
+ "tint": "#f1f8e9",
125
+ "pill": "#e8f5e9",
126
+ "border": "#a5d6a7",
127
+ }
128
+ ),
129
+ "fail": MappingProxyType(
130
+ {
131
+ "fill": "#f44336",
132
+ "dark": "#C62828",
133
+ "tint": "#ffebee",
134
+ "pill": "#ffebee",
135
+ "border": "#ef9a9a",
136
+ }
137
+ ),
138
+ "sim-fail": MappingProxyType(
139
+ {
140
+ "fill": "#d32f2f",
141
+ "dark": "#B71C1C",
142
+ "tint": "#ffebee",
143
+ "pill": "#ffebee",
144
+ "border": "#ef9a9a",
145
+ }
146
+ ),
147
+ "no-ref": MappingProxyType(
148
+ {
149
+ "fill": "#FF9800",
150
+ "dark": "#E65100",
151
+ "tint": "#fff8e1",
152
+ "pill": "#fff8e1",
153
+ "border": "#ffe0b2",
154
+ }
155
+ ),
156
+ "warn": MappingProxyType(
157
+ {
158
+ "fill": "#ffc107",
159
+ "dark": "#8a6d00",
160
+ "tint": "#fff3cd",
161
+ "pill": "#fff3cd",
162
+ "border": "#ffc107",
163
+ }
164
+ ),
165
+ "timed-out": MappingProxyType(
166
+ {
167
+ "fill": "#9C27B0",
168
+ "dark": "#6A1B9A",
169
+ "tint": "#f3e5f5",
170
+ "pill": "#f3e5f5",
171
+ "border": "#e1bee7",
172
+ }
173
+ ),
174
+ "queued": _NEUTRAL,
175
+ "not-run": _NEUTRAL,
176
+ "scoring": _NEUTRAL,
177
+ "running": MappingProxyType(
178
+ {
179
+ "fill": "#2196F3",
180
+ "dark": "#1976D2",
181
+ "tint": "#e3f2fd",
182
+ "pill": "#e3f2fd",
183
+ "border": "#90caf9",
184
+ }
185
+ ),
186
+ "pending": MappingProxyType(
187
+ {
188
+ "fill": "#607D8B",
189
+ "dark": "#607D8B",
190
+ "tint": "#eceff1",
191
+ "pill": "#eceff1",
192
+ "border": "#b0bec5",
193
+ }
194
+ ),
195
+ }
196
+ )
197
+
198
+ ROLES: Mapping[str, Mapping[str, str]] = MappingProxyType(
199
+ {
200
+ "soft_check": MappingProxyType(
201
+ {"bg": "#e8d8f3", "fg": "#5e3a80", "border": "#c4a8df"}
202
+ ),
203
+ "companion": MappingProxyType(
204
+ {"bg": "#d8f3e4", "fg": "#2b6b48", "border": "#a8dfc0"}
205
+ ),
206
+ }
207
+ )
208
+
209
+ #: The tree card's level bars (U5, Q1): level i's bar takes ``level-<(i-1)
210
+ #: % 4 + 1>``, and the deeper ``-on`` shade while its group is hovered.
211
+ #: Blue, violet, cyan, slate — never a status's green, red or amber: a bar
212
+ #: says where a row sits, never how it scored.
213
+ LEVELS: Mapping[str, str] = MappingProxyType(
214
+ {
215
+ "level-1": "#7fb0ec",
216
+ "level-2": "#a990dc",
217
+ "level-3": "#5fbfcf",
218
+ "level-4": "#9aa8b8",
219
+ "level-1-on": "#2f7fd8",
220
+ "level-2-on": "#7650c6",
221
+ "level-3-on": "#1f97ab",
222
+ "level-4-on": "#6b7b8f",
223
+ }
224
+ )
225
+
226
+
227
+ #: How much of a status's FILL a dark theme lays over its surface for each
228
+ #: part that sits behind or around text (D148): the row tint, the pill, the
229
+ #: pill's border. The light table's pastels would be light islands there.
230
+ _DIMMED: Mapping[str, float] = MappingProxyType(
231
+ {"tint": 0.14, "pill": 0.24, "border": 0.6}
232
+ )
233
+
234
+ #: The text a dark theme writes on a status tint or pill, and on the
235
+ #: surface: the status's pastel — the light table's pill border. Warn's
236
+ #: border is its fill, so its pastel is the next lighter amber.
237
+ _PASTELS: Mapping[str, str] = MappingProxyType(
238
+ {
239
+ **{status: parts["border"] for status, parts in STATUS.items()},
240
+ "warn": "#ffd54f",
241
+ }
242
+ )
243
+
244
+
245
+ def _channels(colour: str) -> tuple[int, int, int]:
246
+ return int(colour[1:3], 16), int(colour[3:5], 16), int(colour[5:7], 16)
247
+
248
+
249
+ def _mix(top: str, under: str, alpha: float) -> str:
250
+ """``top`` laid at ``alpha`` over ``under``, both ``#rrggbb``."""
251
+ pairs = zip(_channels(top), _channels(under))
252
+ return "#" + "".join(f"{round(a * alpha + b * (1 - alpha)):02x}" for a, b in pairs)
253
+
254
+
255
+ def status_colours(name: str = "default") -> Mapping[str, Mapping[str, str]]:
256
+ """``STATUS`` as the theme ``name`` draws it. A light theme's is the
257
+ table. A dark one keeps every FILL — the badge is white on it in every
258
+ theme — and dims the rest: tint, pill and border are the fill laid thin
259
+ over the theme's surface (:data:`_DIMMED`), and ``dark``, the text on
260
+ them, is the status's pastel (:data:`_PASTELS`)."""
261
+ if not is_dark(name):
262
+ return STATUS
263
+ surface = THEMES[name]["surface"]
264
+ return {
265
+ status: {
266
+ "fill": parts["fill"],
267
+ "dark": _PASTELS[status],
268
+ **{
269
+ part: _mix(parts["fill"], surface, alpha)
270
+ for part, alpha in _DIMMED.items()
271
+ },
272
+ }
273
+ for status, parts in STATUS.items()
274
+ }
275
+
276
+
277
+ def css_variables(name: str = "default") -> str:
278
+ """The ``:root`` block every page starts its stylesheet with, for the
279
+ bundled theme ``name``."""
280
+ theme = theme_named(name)
281
+ lines = [f" --dstf-{token}: {value};" for token, value in theme.items()]
282
+ lines.append(f" --dstf-font: {FONT_STACK};")
283
+ lines.append(f" --dstf-mono: {MONO_STACK};")
284
+ if is_dark(name):
285
+ # What the browser draws itself — a <select>'s list, a checkbox, a
286
+ # scrollbar — follows this and nothing else.
287
+ lines.append(" color-scheme: dark;")
288
+ for status, parts in status_colours(name).items():
289
+ lines += [
290
+ f" --dstf-{status}-{part}: {value};" for part, value in parts.items()
291
+ ]
292
+ for role, parts in ROLES.items():
293
+ lines += [
294
+ f" --dstf-role-{role}-{part}: {value};" for part, value in parts.items()
295
+ ]
296
+ return ":root {\n" + "\n".join(lines) + "\n}\n"
297
+
298
+
299
+ def level_variables() -> str:
300
+ """The ``:root`` block of the tree card's level tokens: the tool's
301
+ alone (`Shell.page_setup`). In `css_variables` they were eight unused
302
+ properties on every report and dashboard (the U5 review, H5)."""
303
+ lines = [f" --dstf-{name}: {value};" for name, value in LEVELS.items()]
304
+ return ":root {\n" + "\n".join(lines) + "\n}\n"
305
+
306
+
307
+ def quasar_palette(name: str = "default") -> dict[str, str]:
308
+ """``ui.colors(**quasar_palette())``: the tool's Quasar names from the
309
+ table. A dark theme names Quasar's two dark grounds as well — what it
310
+ paints a card, a menu and the page with under ``body--dark``."""
311
+ theme = theme_named(name)
312
+ grounds = (
313
+ {"dark": theme["surface"], "dark_page": theme["page"]} if is_dark(name) else {}
314
+ )
315
+ return {
316
+ **grounds,
317
+ "primary": theme["accent"],
318
+ "secondary": theme["text-secondary"],
319
+ "accent": theme["accent"],
320
+ "positive": STATUS["pass"]["fill"],
321
+ "negative": STATUS["fail"]["fill"],
322
+ "warning": STATUS["warn"]["fill"],
323
+ "info": STATUS["pending"]["fill"],
324
+ }
@@ -0,0 +1,73 @@
1
+ """A plot's criteria rows, read off a `tree_view`: the ONE walk both tiers
2
+ list a plot's criteria by — the report's plot sections and the tool's plot
3
+ cards. Here, not under `authoring/`, because the report draws from it too
4
+ and the report never imports the tool; the tool imports this. Every
5
+ function takes a `tree_view` node (``kind``, ``path``, ``combinator``,
6
+ ``children``), never a raw spec node."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Collection
11
+
12
+
13
+ def leaves_of(view: dict | None) -> list[dict]:
14
+ """The leaf nodes of a `tree_view`, in tree order."""
15
+ if view is None:
16
+ return []
17
+ if view.get("kind") == "leaf":
18
+ return [view]
19
+ return [leaf for child in view.get("children") or [] for leaf in leaves_of(child)]
20
+
21
+
22
+ def names_a_series(leaf: dict) -> bool:
23
+ """Does this leaf of a `tree_view` score a SERIES of the result?
24
+ `resolves_a_variable` over the view's dict: a stored-scalar leaf's
25
+ ``variable`` is free text naming the leaf, and a plot of a variable it
26
+ happens to spell showed that leaf as its criterion."""
27
+ from ..comparison.tree_spec import reads_a_statistic
28
+
29
+ return not reads_a_statistic(str(leaf.get("metric")), leaf.get("params") or {})
30
+
31
+
32
+ def ancestor_kinds(view: dict | None) -> dict[str, tuple[str, ...]]:
33
+ """Each node's path -> its ancestors' combinators, the root's first:
34
+ where in the tree a criteria row's leaf sits, which its row is led by
35
+ (``and › k-of-n ›``). A plot's rows are its subject's leaves, so the
36
+ tree's structure is read off the row, not off the order of the rows."""
37
+ out: dict[str, tuple[str, ...]] = {}
38
+
39
+ def walk(node: dict, above: tuple[str, ...]) -> None:
40
+ out[str(node.get("path"))] = above
41
+ kind = node.get("combinator")
42
+ for child in node.get("children") or []:
43
+ walk(child, above + ((str(kind),) if kind else ()))
44
+
45
+ if view is not None:
46
+ walk(view, ())
47
+ return out
48
+
49
+
50
+ def crumb_text(kinds: tuple[str, ...]) -> str:
51
+ """``and › k-of-n ›`` — a row's breadcrumb; empty for a root leaf."""
52
+ return " ".join(f"{kind} ›" for kind in kinds)
53
+
54
+
55
+ def criteria_rows(view: dict | None, subject: Collection[str]) -> list[dict]:
56
+ """A plot's criteria: the leaves of ``view`` on a member of
57
+ ``subject``, in TREE order, each with ``crumbs`` — its ancestors'
58
+ kinds, the root's first (``and › k-of-n ›``). The tree's structure is
59
+ read off a row, not off the order of the rows: a family's rows are
60
+ where the tree has them. A stored-scalar leaf is no row of any plot
61
+ (:func:`names_a_series`): its ``variable`` names the leaf, not a
62
+ series. Both tiers list a plot's criteria by this."""
63
+ wanted = set(subject)
64
+ kinds = ancestor_kinds(view)
65
+ return [
66
+ {
67
+ "leaf": leaf,
68
+ "variable": leaf.get("variable"),
69
+ "crumbs": crumb_text(kinds.get(str(leaf.get("path")), ())),
70
+ }
71
+ for leaf in leaves_of(view)
72
+ if leaf.get("variable") in wanted and names_a_series(leaf)
73
+ ]
@@ -0,0 +1,124 @@
1
+ """``verdicts.json``: what a scoring ``run`` or ``compare`` decided, as one
2
+ file in the work dir — every test's status word and its criteria tree with
3
+ each node's verdict and numbers, and the run's totals.
4
+
5
+ For a script or an agent. A plain run used to leave no per-criterion
6
+ result on disk at all (the tree was written only by ``--report``, into one
7
+ ``comparison_data.json`` per test), and ``status.json`` says ``"passed"``
8
+ of a test whose comparison FAILED: it records that the simulation ran.
9
+
10
+ Nothing here is a second computation. The status word is the console's
11
+ (:mod:`.status`), and a test's ``tree`` is the dict
12
+ :func:`~.context_builder.build_tree_view` returns — the ``tree_view`` of
13
+ that test's ``comparison_data.json`` and the tree the tool shows.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from collections.abc import Iterable, Mapping
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from ..comparison.comparator import TestComparison
25
+ from ..storage.ref_index import _atomic_write_text
26
+ from .context_builder import build_tree_view, test_texts
27
+ from .status import STATUS_LABELS, STATUS_ORDER, classify, count_by_status, exit_code
28
+
29
+ #: The file's name, beside ``status.json`` in the work dir.
30
+ VERDICTS_FILE = "verdicts.json"
31
+
32
+
33
+ def status_word(comp: TestComparison) -> str:
34
+ """The word the console prints for ``comp``: ``PASS``, ``FAIL``,
35
+ ``SIM_FAIL``, ``NO_REF``, ``NOT RUN``."""
36
+ return STATUS_LABELS[classify(comp)]
37
+
38
+
39
+ def test_verdict(comp: TestComparison, test: Any, result: Any) -> dict:
40
+ """One test of the file: its model id, its status word, what the
41
+ comparison said where there is no tree to say it (a failed simulation's
42
+ error, a refusal, "No reference baseline stored"), its warnings, and
43
+ its criteria ``tree`` — None for a test nothing was compared for."""
44
+ return {
45
+ "model_id": comp.model_id,
46
+ "test_id": comp.test_id,
47
+ "status": status_word(comp),
48
+ "message": comp.error_message or None,
49
+ "warnings": [w.text for w in comp.warnings],
50
+ "tree": build_tree_view(
51
+ list(comp.variables),
52
+ test,
53
+ comp.metric_tree,
54
+ test_texts(comp.variables, result),
55
+ ),
56
+ }
57
+
58
+
59
+ def build_verdicts(
60
+ comparisons: Iterable[TestComparison],
61
+ tests: Iterable[Any],
62
+ results: Mapping[str, Any] | None = None,
63
+ metadata: Mapping[str, Any] | None = None,
64
+ ) -> dict:
65
+ """The document :func:`write_verdicts` writes: the scored tests in the
66
+ order given, and their totals by status word — every word of the
67
+ console's summary, zero included — with the exit code they give."""
68
+ comps = list(comparisons)
69
+ by_model = {t.model_id: t for t in tests}
70
+ counts = count_by_status(comps)
71
+ totals = {
72
+ STATUS_LABELS[s]: counts[s]
73
+ for s in STATUS_ORDER
74
+ if s in STATUS_LABELS and (counts[s] or s in _ALWAYS)
75
+ }
76
+ return {
77
+ "written_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
78
+ # The run these verdicts are of: status.json's provenance block
79
+ # (backend, tool version, os), or null where there is none.
80
+ "metadata": dict(metadata) if metadata else None,
81
+ "totals": {"total": len(comps), **totals},
82
+ "exit_code": exit_code(comps),
83
+ "tests": [
84
+ test_verdict(c, by_model.get(c.model_id), (results or {}).get(c.model_id))
85
+ for c in comps
86
+ ],
87
+ }
88
+
89
+
90
+ #: The words a total is always given for, a zero included: what a compare
91
+ #: can say of a test. (A QUEUED or RUNNING row is the live page's.)
92
+ _ALWAYS = frozenset({"PASS", "FAIL", "SIM_FAIL", "NO_REF", "NOT RUN"})
93
+
94
+
95
+ def write_verdicts(
96
+ work_dir: Path,
97
+ comparisons: Iterable[TestComparison],
98
+ tests: Iterable[Any],
99
+ results: Mapping[str, Any] | None = None,
100
+ ) -> Path:
101
+ """Write ``<work_dir>/verdicts.json`` — atomically, as strict JSON (a
102
+ sentinel's inf/nan is ``null``) — and stamp each test's verdict into
103
+ ``status.json`` beside its simulation status. Returns the path."""
104
+ from ..backends.progress import read_status, stamp_verdicts
105
+ from .report_suite import _json_sanitize
106
+
107
+ comps = list(comparisons)
108
+ snapshot = read_status(work_dir) or {}
109
+ document = build_verdicts(comps, tests, results, snapshot.get("metadata"))
110
+ path = work_dir / VERDICTS_FILE
111
+ work_dir.mkdir(parents=True, exist_ok=True)
112
+ _atomic_write_text(
113
+ path, json.dumps(_json_sanitize(document), indent=2, allow_nan=False) + "\n"
114
+ )
115
+ stamp_verdicts(work_dir, {c.model_id: status_word(c) for c in comps})
116
+ return path
117
+
118
+
119
+ def remove_verdicts(work_dir: Path) -> None:
120
+ """Remove a ``verdicts.json`` that a run or compare is about to make
121
+ stale — with the per-test pages, and for the same reason: a file that
122
+ outlives the results it describes says what is no longer true. The
123
+ command that scores writes it again."""
124
+ (work_dir / VERDICTS_FILE).unlink(missing_ok=True)
File without changes