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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
dstf/reporting/theme.py
ADDED
|
@@ -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)
|
dstf/storage/__init__.py
ADDED
|
File without changes
|