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
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""The Test page's rail (the owner's ruling, 2026-09-25; DESIGN.md "The
|
|
2
|
+
Variables drawer"): 40 px of labelled buttons down the left edge, and ONE
|
|
3
|
+
drawer at a time beside them, 320 px, which pushes the plots narrower
|
|
4
|
+
rather than covering them (`behavior=desktop`: below Quasar's 1024 px
|
|
5
|
+
breakpoint a drawer otherwise turns into an overlay). Plotly follows the
|
|
6
|
+
narrower slot by itself — NiceGUI's plotly element observes its size. Which
|
|
7
|
+
drawer is open is the tab's (`TabState.drawer`), so a switch of test keeps
|
|
8
|
+
it open."""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from functools import partial
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
from urllib.parse import quote
|
|
16
|
+
|
|
17
|
+
import regex
|
|
18
|
+
from nicegui import ui
|
|
19
|
+
|
|
20
|
+
from ...reporting.status import (
|
|
21
|
+
FAIL,
|
|
22
|
+
NO_REF,
|
|
23
|
+
NOT_RUN,
|
|
24
|
+
PASS,
|
|
25
|
+
SCORING_FAILED,
|
|
26
|
+
SIM_FAIL,
|
|
27
|
+
STATUS_CLASSES,
|
|
28
|
+
classify,
|
|
29
|
+
)
|
|
30
|
+
from ..needle import matcher
|
|
31
|
+
from .drawer_controls import TIP_ABOVE, filter_box, menu_button
|
|
32
|
+
from .test_list import Row, TestList, _name, _package
|
|
33
|
+
from .variables_drawer import VariablesDrawer
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from .test_page import TestPage
|
|
37
|
+
|
|
38
|
+
RAIL = 40
|
|
39
|
+
#: A dragged panel's bounds (K5): at least ``MIN_PANEL`` px, at most
|
|
40
|
+
#: ``MAX_FRACTION`` of the window AND the window less the rail and
|
|
41
|
+
#: ``MIN_PLOTS`` px for the page's plots column (at 900 px, 60% left the
|
|
42
|
+
#: plots 320 px and a context chip's × past its card). The browser clamps
|
|
43
|
+
#: to both, which only it can measure, and sends the window's width with
|
|
44
|
+
#: the release; the server holds the same bounds again (`max_panel`).
|
|
45
|
+
MIN_PANEL, MAX_FRACTION, MIN_PLOTS = 240, 0.6, 480
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def max_panel(window: float) -> float:
|
|
49
|
+
"""The widest a dragged panel may be in a ``window`` px wide window —
|
|
50
|
+
`_WIDTH`'s ``max``. The floor wins when a narrow window leaves less."""
|
|
51
|
+
return min(MAX_FRACTION * window, window - RAIL - MIN_PLOTS)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
#: The drawer's width in the BROWSER (K5), installed by `Rail.__init__` as
|
|
55
|
+
#: `window.__dstfDrawer`: the visible panel (`offsetParent`: both drawers'
|
|
56
|
+
#: panels are in the row, one hidden, B10), the clamp to `MIN_PANEL` and
|
|
57
|
+
#: `max_panel` — only the browser knows the window's width — and `apply`,
|
|
58
|
+
#: which sets the panel's width and the drawer's `width` prop on NiceGUI's
|
|
59
|
+
#: element store (`mounted_app.elements`, reactive, so Quasar re-lays the
|
|
60
|
+
#: page out and Plotly's observer reflows each plot) while the pointer or a
|
|
61
|
+
#: key moves it. `mounted_app.elements[id].props` is NiceGUI 3.16's
|
|
62
|
+
#: INTERNAL client-side store, not an API: the lookup is guarded (`store`),
|
|
63
|
+
#: so on a NiceGUI that renames it the panel still moves and the release
|
|
64
|
+
#: still sends the width — only the drawer lags the panel until the
|
|
65
|
+
#: server's update (the Playwright test reading the drawer's width
|
|
66
|
+
#: mid-drag fails first). `fit` clamps a width the window no longer holds
|
|
67
|
+
#: — the tab's width from a wider window — on load (the width the server
|
|
68
|
+
#: has, sent at once) and on a window `resize` (sent once, 150 ms after
|
|
69
|
+
#: the last).
|
|
70
|
+
_WIDTH = """(() => {
|
|
71
|
+
const RAIL = __RAIL__, MIN = __MIN__;
|
|
72
|
+
const panel = () => [...document.querySelectorAll('.dstf-drawer-panel')]
|
|
73
|
+
.find((p) => p.offsetParent !== null);
|
|
74
|
+
const max = () => Math.min(__MAX__ * window.innerWidth, window.innerWidth - RAIL - __PLOTS__);
|
|
75
|
+
const clamp = (w) => Math.round(Math.max(MIN, Math.min(w, max())));
|
|
76
|
+
const width = (p) => Math.round(p.getBoundingClientRect().width);
|
|
77
|
+
// `mounted_app` is a top-level `let` of nicegui.js — a global binding,
|
|
78
|
+
// no property of `window` — hence the try.
|
|
79
|
+
const store = () => {
|
|
80
|
+
try { return mounted_app?.elements; } catch { return undefined; }
|
|
81
|
+
};
|
|
82
|
+
const bounds = () => document.getElementById('__GRIP__')
|
|
83
|
+
?.setAttribute('aria-valuemax', Math.round(Math.max(MIN, max())));
|
|
84
|
+
const apply = (p, w) => {
|
|
85
|
+
p.style.width = w + 'px';
|
|
86
|
+
const drawer = store()?.[__DRAWER__];
|
|
87
|
+
if (drawer) drawer.props.width = RAIL + w;
|
|
88
|
+
document.getElementById('__GRIP__')?.setAttribute('aria-valuenow', w);
|
|
89
|
+
bounds();
|
|
90
|
+
};
|
|
91
|
+
const send = (w) => emitEvent('dstf_drawer_width', w, window.innerWidth);
|
|
92
|
+
let timer = null;
|
|
93
|
+
const fit = (known) => {
|
|
94
|
+
const p = panel();
|
|
95
|
+
const w = known ?? (p ? width(p) : null);
|
|
96
|
+
bounds();
|
|
97
|
+
if (w === null) return;
|
|
98
|
+
const c = clamp(w);
|
|
99
|
+
if (c === w) return;
|
|
100
|
+
if (p) apply(p, c);
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (known !== undefined) send(c);
|
|
103
|
+
else timer = setTimeout(() => send(c), 150);
|
|
104
|
+
};
|
|
105
|
+
window.__dstfDrawer = {panel, max, clamp, width, apply, fit};
|
|
106
|
+
if (!window.__dstfDrawerResize) {
|
|
107
|
+
window.__dstfDrawerResize = true;
|
|
108
|
+
window.addEventListener('resize', () => window.__dstfDrawer?.fit());
|
|
109
|
+
}
|
|
110
|
+
})();"""
|
|
111
|
+
|
|
112
|
+
#: The grip's drag: the panel follows the pointer (`apply`), and the
|
|
113
|
+
#: release sends the width to Python once, which sets the same values and
|
|
114
|
+
#: keeps them in `TabState`.
|
|
115
|
+
_DRAG = """(e) => {
|
|
116
|
+
if (e.button !== 0) return;
|
|
117
|
+
const d = window.__dstfDrawer, panel = d?.panel();
|
|
118
|
+
if (!panel) return;
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
const handle = e.currentTarget;
|
|
121
|
+
const x0 = e.clientX, w0 = panel.getBoundingClientRect().width;
|
|
122
|
+
let w = w0;
|
|
123
|
+
handle.setPointerCapture(e.pointerId);
|
|
124
|
+
const move = (m) => {
|
|
125
|
+
w = d.clamp(w0 + m.clientX - x0);
|
|
126
|
+
d.apply(panel, w);
|
|
127
|
+
};
|
|
128
|
+
const up = () => {
|
|
129
|
+
handle.removeEventListener('pointermove', move);
|
|
130
|
+
handle.removeEventListener('pointerup', up);
|
|
131
|
+
emit(w, window.innerWidth);
|
|
132
|
+
};
|
|
133
|
+
handle.addEventListener('pointermove', move);
|
|
134
|
+
handle.addEventListener('pointerup', up);
|
|
135
|
+
}"""
|
|
136
|
+
|
|
137
|
+
#: The grip from the keyboard (a focusable `separator`, WAI-ARIA's window
|
|
138
|
+
#: splitter): ←/→ 16 px, with Shift 64, Home the narrowest, End the
|
|
139
|
+
#: widest; the panel follows each key, and the key's release sends the
|
|
140
|
+
#: width once — a held → sends one width, not one per repeat. A key moves
|
|
141
|
+
#: the grip's `dirty` mark and a send clears it; leaving the grip sends a
|
|
142
|
+
#: width still marked: a key held while the focus moves (→ held, Tab) is
|
|
143
|
+
#: released on another element, and the width it left never reached the
|
|
144
|
+
#: server.
|
|
145
|
+
_STEP, _BIG_STEP = 16, 64
|
|
146
|
+
_GRIP_KEYS = ("ArrowLeft", "ArrowRight", "Home", "End")
|
|
147
|
+
_KEYDOWN = """(e) => {
|
|
148
|
+
const d = window.__dstfDrawer, panel = d?.panel();
|
|
149
|
+
if (!panel) return;
|
|
150
|
+
const w = d.width(panel), step = e.shiftKey ? __BIG__ : __STEP__;
|
|
151
|
+
const to = {ArrowLeft: w - step, ArrowRight: w + step, Home: __MIN__, End: d.max()}[e.key];
|
|
152
|
+
if (to === undefined) return;
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
d.apply(panel, d.clamp(to));
|
|
155
|
+
e.currentTarget.dataset.dirty = '1';
|
|
156
|
+
}"""
|
|
157
|
+
_KEYUP = """(e) => {
|
|
158
|
+
if (!__KEYS__.includes(e.key)) return;
|
|
159
|
+
const d = window.__dstfDrawer, panel = d?.panel();
|
|
160
|
+
delete e.currentTarget.dataset.dirty;
|
|
161
|
+
if (panel) emit(d.width(panel), window.innerWidth);
|
|
162
|
+
}"""
|
|
163
|
+
_BLUR = """(e) => {
|
|
164
|
+
if (!e.currentTarget.dataset.dirty) return;
|
|
165
|
+
delete e.currentTarget.dataset.dirty;
|
|
166
|
+
const d = window.__dstfDrawer, panel = d?.panel();
|
|
167
|
+
if (panel) emit(d.width(panel), window.innerWidth);
|
|
168
|
+
}"""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
EDITED = "EDITED"
|
|
172
|
+
#: The Tests drawer's status pills, in the Suite's order, and EDITED.
|
|
173
|
+
PILLS = (PASS, FAIL, SIM_FAIL, NO_REF, NOT_RUN, EDITED)
|
|
174
|
+
#: Each pill's `.dstf-pill-<status>` tokens; EDITED reads as the Suite's
|
|
175
|
+
#: Edits cell and the rows' ``N unsaved`` pill do.
|
|
176
|
+
PILL_CSS = {**{s: STATUS_CLASSES[s] for s in PILLS if s != EDITED}, EDITED: "warn"}
|
|
177
|
+
#: "Status, failures first": FAIL, SIM_FAIL, scoring failed, NO_REF, PASS,
|
|
178
|
+
#: NOT RUN (no result, D145), then no verdict yet — NOT `STATUS_ORDER`,
|
|
179
|
+
#: which lists PASS first.
|
|
180
|
+
FAILURES_FIRST = (FAIL, SIM_FAIL, SCORING_FAILED, NO_REF, PASS, NOT_RUN, "")
|
|
181
|
+
SORTS = (
|
|
182
|
+
("package", "Package order"),
|
|
183
|
+
("name", "Name A→Z"),
|
|
184
|
+
("status", "Status, failures first"),
|
|
185
|
+
)
|
|
186
|
+
NAMES = (("short", "Short"), ("full", "Full"))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestsDrawer:
|
|
190
|
+
"""The Suite page's list, without checkboxes, and the drawers' one
|
|
191
|
+
control set (the owner's ruling Q7): the filter with `.*` / `Aa`, sort,
|
|
192
|
+
group and names, and compact status pills with EDITED — every control
|
|
193
|
+
the tab's (`TabState`), so a switch of test keeps them. This test is
|
|
194
|
+
highlighted; a click opens that test in this tab, and pending edits are
|
|
195
|
+
the Session's and survive the switch."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, page: TestPage, panel: ui.column) -> None:
|
|
198
|
+
self.page = page
|
|
199
|
+
state = page.state
|
|
200
|
+
#: The filter's kept ids and its sentence, and what they were
|
|
201
|
+
#: computed from (`_kept`).
|
|
202
|
+
self.kept: tuple[object, set[str], str | None] | None = None
|
|
203
|
+
with panel:
|
|
204
|
+
ui.label("Tests").classes("text-subtitle2")
|
|
205
|
+
self.box = filter_box(
|
|
206
|
+
state.tests_needle,
|
|
207
|
+
state.tests_regex,
|
|
208
|
+
state.tests_case,
|
|
209
|
+
self._on_filter,
|
|
210
|
+
"filter tests",
|
|
211
|
+
"tests-drawer:",
|
|
212
|
+
)
|
|
213
|
+
with ui.row().classes("items-center no-wrap gap-0 w-full"):
|
|
214
|
+
self.sort = menu_button(
|
|
215
|
+
"sort",
|
|
216
|
+
"Sort",
|
|
217
|
+
SORTS,
|
|
218
|
+
state.tests_sort,
|
|
219
|
+
self._on_sort,
|
|
220
|
+
"tests-drawer:sort",
|
|
221
|
+
)
|
|
222
|
+
self.group = (
|
|
223
|
+
ui.button(icon="account_tree", on_click=self._on_group)
|
|
224
|
+
.props("flat dense round")
|
|
225
|
+
.mark("tests-drawer:group")
|
|
226
|
+
)
|
|
227
|
+
self.group.props["aria-label"] = "Group by package"
|
|
228
|
+
with self.group:
|
|
229
|
+
self.group_tip = ui.tooltip("").props(TIP_ABOVE)
|
|
230
|
+
self.names = menu_button(
|
|
231
|
+
"label",
|
|
232
|
+
"Names",
|
|
233
|
+
NAMES,
|
|
234
|
+
"short" if state.tests_short else "full",
|
|
235
|
+
self._on_names,
|
|
236
|
+
"tests-drawer:names",
|
|
237
|
+
)
|
|
238
|
+
with ui.row().classes("items-center gap-1 w-full"):
|
|
239
|
+
self.pills: dict[str, ui.button] = {}
|
|
240
|
+
for word in PILLS:
|
|
241
|
+
self.pills[word] = (
|
|
242
|
+
ui.button(on_click=partial(self._on_pill, word))
|
|
243
|
+
.props("flat dense no-caps")
|
|
244
|
+
.classes(
|
|
245
|
+
f"dstf-count-pill dstf-pill dstf-pill-{PILL_CSS[word]}"
|
|
246
|
+
)
|
|
247
|
+
# One word: NiceGUI splits a marker on whitespace,
|
|
248
|
+
# and "NOT RUN" has a space.
|
|
249
|
+
.mark(f"tests-drawer:pill:{word.replace(' ', '_')}")
|
|
250
|
+
)
|
|
251
|
+
self.list = TestList(
|
|
252
|
+
page.shell,
|
|
253
|
+
on_open=self._open,
|
|
254
|
+
current=page.model_id,
|
|
255
|
+
marker="tests-drawer:",
|
|
256
|
+
)
|
|
257
|
+
self._paint_group()
|
|
258
|
+
self._rebuild()
|
|
259
|
+
|
|
260
|
+
# -- the controls ------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
def _on_filter(self, needle: str, regex: bool, case: bool) -> None:
|
|
263
|
+
state = self.page.state
|
|
264
|
+
state.tests_needle, state.tests_regex, state.tests_case = needle, regex, case
|
|
265
|
+
self._filter()
|
|
266
|
+
|
|
267
|
+
def _on_sort(self, value: str) -> None:
|
|
268
|
+
self.page.state.tests_sort = value
|
|
269
|
+
self._rebuild()
|
|
270
|
+
|
|
271
|
+
def _on_group(self) -> None:
|
|
272
|
+
state = self.page.state
|
|
273
|
+
state.tests_grouped = not state.tests_grouped
|
|
274
|
+
self._paint_group()
|
|
275
|
+
self._rebuild()
|
|
276
|
+
|
|
277
|
+
def _paint_group(self) -> None:
|
|
278
|
+
"""Pressed while grouped; the tooltip says what a click does."""
|
|
279
|
+
grouped = self.page.state.tests_grouped
|
|
280
|
+
self.group.props(f'aria-pressed="{str(grouped).lower()}"')
|
|
281
|
+
self.group_tip.set_text("Show flat" if grouped else "Group by package")
|
|
282
|
+
|
|
283
|
+
def _on_names(self, value: str) -> None:
|
|
284
|
+
self.page.state.tests_short = value == "short"
|
|
285
|
+
self._rebuild()
|
|
286
|
+
|
|
287
|
+
def _on_pill(self, word: str) -> None:
|
|
288
|
+
self.page.state.tests_pills.symmetric_difference_update({word})
|
|
289
|
+
self._filter()
|
|
290
|
+
|
|
291
|
+
# -- the rows ----------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _ids(self) -> list[str]:
|
|
294
|
+
"""The tests in the chosen order; package order is the shell's."""
|
|
295
|
+
ids = self.page.shell.visible_tests()
|
|
296
|
+
state = self.page.state
|
|
297
|
+
if state.tests_sort == "name":
|
|
298
|
+
return sorted(
|
|
299
|
+
ids, key=lambda m: (_name(m, state.tests_short).casefold(), m)
|
|
300
|
+
)
|
|
301
|
+
if state.tests_sort == "status":
|
|
302
|
+
position = {m: i for i, m in enumerate(ids)}
|
|
303
|
+
return sorted(ids, key=lambda m: (_rank(self._verdict(m)), position[m]))
|
|
304
|
+
return ids
|
|
305
|
+
|
|
306
|
+
def _verdict(self, model_id: str) -> str:
|
|
307
|
+
"""What the row's badge says (`TestList.paint`), read before the
|
|
308
|
+
rows exist: the order is decided first."""
|
|
309
|
+
comp = self.page.shell.session.comparisons.get(model_id)
|
|
310
|
+
if comp is not None:
|
|
311
|
+
return classify(comp)
|
|
312
|
+
return SCORING_FAILED if model_id in self.page.shell.load.failed_models else ""
|
|
313
|
+
|
|
314
|
+
def _drawn(self) -> list[str]:
|
|
315
|
+
"""The order the rows are drawn in now: `_ids`, by package first
|
|
316
|
+
when grouped (`TestList.rebuild`)."""
|
|
317
|
+
ids = self._ids()
|
|
318
|
+
if not self.page.state.tests_grouped:
|
|
319
|
+
return ids
|
|
320
|
+
position = {m: i for i, m in enumerate(ids)}
|
|
321
|
+
return sorted(ids, key=lambda m: (_package(m), position[m]))
|
|
322
|
+
|
|
323
|
+
def _rebuild(self) -> None:
|
|
324
|
+
"""The rows from scratch in the chosen order and grouping, and the
|
|
325
|
+
tab's filter APPLIED to them: a drawer built on the next test's page
|
|
326
|
+
showed the filter in its box over every row until some event
|
|
327
|
+
happened to apply it."""
|
|
328
|
+
state = self.page.state
|
|
329
|
+
self.list.rebuild(
|
|
330
|
+
self._ids(), grouped=state.tests_grouped, short=state.tests_short
|
|
331
|
+
)
|
|
332
|
+
self._filter()
|
|
333
|
+
|
|
334
|
+
def _open(self, model_id: str) -> None:
|
|
335
|
+
ui.navigate.to(f"/test/{quote(model_id, safe='')}")
|
|
336
|
+
|
|
337
|
+
def _kept(self) -> tuple[set[str], str | None]:
|
|
338
|
+
"""The ids the filter keeps, and the sentence under the box — every
|
|
339
|
+
id, and why, for a bad pattern or one that ran out of time: the
|
|
340
|
+
drawer hides nothing then. Cached on the needle, its toggles and
|
|
341
|
+
the SET of ids, never their order: events repaint the rows as a
|
|
342
|
+
queue's tests end, a rescore under the status sort reorders them,
|
|
343
|
+
and a pathological pattern cost 0.79 s of CPU per pass over 331 ids
|
|
344
|
+
(the plan's executing review)."""
|
|
345
|
+
state = self.page.state
|
|
346
|
+
ids = frozenset(self.list.rows)
|
|
347
|
+
key = (state.tests_needle, state.tests_regex, state.tests_case, ids)
|
|
348
|
+
if self.kept is None or self.kept[0] != key:
|
|
349
|
+
why: str | None = None
|
|
350
|
+
try:
|
|
351
|
+
match = matcher(
|
|
352
|
+
state.tests_needle, regex=state.tests_regex, case=state.tests_case
|
|
353
|
+
)
|
|
354
|
+
kept = {m for m in ids if match(m)}
|
|
355
|
+
except regex.error as e:
|
|
356
|
+
kept, why = set(ids), f"not a regular expression: {e}"
|
|
357
|
+
except TimeoutError:
|
|
358
|
+
kept, why = set(ids), "the pattern took too long; simplify it"
|
|
359
|
+
self.kept = (key, kept, why)
|
|
360
|
+
return self.kept[1], self.kept[2]
|
|
361
|
+
|
|
362
|
+
def _filter(self) -> None:
|
|
363
|
+
"""The filter and the pills over the rows; the pills count every
|
|
364
|
+
row, whatever the filter. The statuses pressed are OR'd, and EDITED
|
|
365
|
+
with them — the Suite's rule (`suite_grid.js`). A test the open
|
|
366
|
+
could not score is SIM_FAIL to the pills, counted and kept, as the
|
|
367
|
+
Suite's row says it (`suite_rows`); the status sort keeps it a
|
|
368
|
+
place of its own (`FAILURES_FIRST`)."""
|
|
369
|
+
state = self.page.state
|
|
370
|
+
counts = dict.fromkeys(PILLS, 0)
|
|
371
|
+
for row in self.list.rows.values():
|
|
372
|
+
pill = _pill_word(row)
|
|
373
|
+
if pill in counts:
|
|
374
|
+
counts[pill] += 1
|
|
375
|
+
if row.pending:
|
|
376
|
+
counts[EDITED] += 1
|
|
377
|
+
for word, button in self.pills.items():
|
|
378
|
+
on = word in state.tests_pills
|
|
379
|
+
button.set_text(f"{word} {counts[word]}")
|
|
380
|
+
button.props(f'aria-pressed="{str(on).lower()}"')
|
|
381
|
+
if on:
|
|
382
|
+
button.classes(add="dstf-count-pill-on")
|
|
383
|
+
else:
|
|
384
|
+
button.classes(remove="dstf-count-pill-on")
|
|
385
|
+
# One switched on stays enabled at 0, so it can be switched off.
|
|
386
|
+
button.set_enabled(counts[word] > 0 or on)
|
|
387
|
+
kept, why = self._kept()
|
|
388
|
+
self.box.say(why)
|
|
389
|
+
pills = state.tests_pills
|
|
390
|
+
|
|
391
|
+
def keep(row: Row) -> bool:
|
|
392
|
+
if row.model_id not in kept:
|
|
393
|
+
return False
|
|
394
|
+
if not pills:
|
|
395
|
+
return True
|
|
396
|
+
return _pill_word(row) in pills or (EDITED in pills and row.pending > 0)
|
|
397
|
+
|
|
398
|
+
self.list.filter(keep)
|
|
399
|
+
|
|
400
|
+
def handle(self, event: dict[str, Any]) -> None:
|
|
401
|
+
"""The Suite page's rules for its rows (`SuitePage.handle`): a `spec`
|
|
402
|
+
that moved the test set rebuilds them, and one that did not repaints
|
|
403
|
+
the rows whose edits it moved; a scored model repaints its own row.
|
|
404
|
+
Under the status sort, a verdict that moved the order rebuilds
|
|
405
|
+
them."""
|
|
406
|
+
kind = event["event"]
|
|
407
|
+
if kind == "spec":
|
|
408
|
+
if set(self.list.rows) != set(self.page.shell.visible_tests()):
|
|
409
|
+
self._rebuild()
|
|
410
|
+
return
|
|
411
|
+
for model_id in event.get("models") or list(self.list.rows):
|
|
412
|
+
self.list.paint(str(model_id))
|
|
413
|
+
elif kind == "loading":
|
|
414
|
+
if event.get("phase") == "results":
|
|
415
|
+
if not self.list.rows:
|
|
416
|
+
self._rebuild() # a drawer opened before the spec event
|
|
417
|
+
return
|
|
418
|
+
self.list.paint(str(event["model"]))
|
|
419
|
+
elif kind == "results":
|
|
420
|
+
for model_id in event.get("models", ()):
|
|
421
|
+
self.list.paint(str(model_id))
|
|
422
|
+
if self.page.state.tests_sort == "status" and list(self.list.rows) != (
|
|
423
|
+
self._drawn()
|
|
424
|
+
):
|
|
425
|
+
self._rebuild()
|
|
426
|
+
return
|
|
427
|
+
self._filter()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _pill_word(row: Row) -> str:
|
|
431
|
+
"""The status pill a row counts under: its verdict, SIM_FAIL for
|
|
432
|
+
"scoring failed"."""
|
|
433
|
+
return SIM_FAIL if row.verdict == SCORING_FAILED else row.verdict
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _rank(verdict: str) -> int:
|
|
437
|
+
"""Where "Status, failures first" puts a verdict: a word it does not
|
|
438
|
+
list goes last."""
|
|
439
|
+
return (
|
|
440
|
+
FAILURES_FIRST.index(verdict)
|
|
441
|
+
if verdict in FAILURES_FIRST
|
|
442
|
+
else len(FAILURES_FIRST)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class Rail:
|
|
447
|
+
"""The rail and its drawers' panels. A drawer's content is built the
|
|
448
|
+
first time it opens."""
|
|
449
|
+
|
|
450
|
+
def __init__(self, page: TestPage) -> None:
|
|
451
|
+
self.page = page
|
|
452
|
+
self.tests: TestsDrawer | None = None
|
|
453
|
+
self.variables: VariablesDrawer | None = None
|
|
454
|
+
self.buttons: dict[str, ui.button] = {}
|
|
455
|
+
self.panels: dict[str, ui.column] = {}
|
|
456
|
+
self.drawer = (
|
|
457
|
+
ui.left_drawer(value=True)
|
|
458
|
+
.props(f"width={RAIL} behavior=desktop bordered")
|
|
459
|
+
.classes("dstf-rail-drawer")
|
|
460
|
+
)
|
|
461
|
+
with (
|
|
462
|
+
self.drawer,
|
|
463
|
+
ui.row().classes("dstf-rail-row no-wrap gap-0 items-stretch w-full h-full"),
|
|
464
|
+
):
|
|
465
|
+
with ui.column().classes("dstf-rail gap-1 items-center"):
|
|
466
|
+
for kind, icon, text, tip in (
|
|
467
|
+
("tests", "format_list_bulleted", "Tests", None),
|
|
468
|
+
("variables", "data_array", "Vars", "Variables"),
|
|
469
|
+
):
|
|
470
|
+
with (
|
|
471
|
+
ui.button(on_click=lambda k=kind: self.toggle(k))
|
|
472
|
+
.props("flat dense no-caps stack")
|
|
473
|
+
.mark(f"rail:{kind}") as button
|
|
474
|
+
):
|
|
475
|
+
ui.icon(icon)
|
|
476
|
+
ui.label(text).classes("dstf-rail-label")
|
|
477
|
+
if tip is not None: # "Vars" is short for it
|
|
478
|
+
ui.tooltip(tip)
|
|
479
|
+
self.buttons[kind] = button
|
|
480
|
+
for kind in ("tests", "variables"):
|
|
481
|
+
panel = (
|
|
482
|
+
ui.column()
|
|
483
|
+
.classes("dstf-drawer-panel q-pa-sm gap-2")
|
|
484
|
+
.mark(f"drawer:{kind}")
|
|
485
|
+
)
|
|
486
|
+
panel.set_visibility(False)
|
|
487
|
+
self.panels[kind] = panel
|
|
488
|
+
self.grip = (
|
|
489
|
+
ui.element("div")
|
|
490
|
+
.classes("dstf-drawer-handle")
|
|
491
|
+
.mark("drawer-handle")
|
|
492
|
+
.on(
|
|
493
|
+
"pointerdown",
|
|
494
|
+
lambda e: self.resize(*e.args),
|
|
495
|
+
js_handler=_DRAG,
|
|
496
|
+
)
|
|
497
|
+
.on(
|
|
498
|
+
"keydown",
|
|
499
|
+
js_handler=_KEYDOWN.replace("__MIN__", str(MIN_PANEL))
|
|
500
|
+
.replace("__STEP__", str(_STEP))
|
|
501
|
+
.replace("__BIG__", str(_BIG_STEP)),
|
|
502
|
+
)
|
|
503
|
+
.on(
|
|
504
|
+
"keyup",
|
|
505
|
+
lambda e: self.resize(*e.args),
|
|
506
|
+
js_handler=_KEYUP.replace("__KEYS__", json.dumps(_GRIP_KEYS)),
|
|
507
|
+
)
|
|
508
|
+
.on("blur", lambda e: self.resize(*e.args), js_handler=_BLUR)
|
|
509
|
+
)
|
|
510
|
+
# A window splitter (WAI-ARIA): focusable, its value the panel's
|
|
511
|
+
# width — the most is the browser's (`_WIDTH`'s `bounds`).
|
|
512
|
+
grip = self.grip.props
|
|
513
|
+
grip["role"] = "separator"
|
|
514
|
+
grip["aria-orientation"] = "vertical"
|
|
515
|
+
grip["aria-label"] = "Resize the drawer"
|
|
516
|
+
grip["tabindex"] = "0"
|
|
517
|
+
grip["aria-valuemin"] = str(MIN_PANEL)
|
|
518
|
+
self.grip.set_visibility(False)
|
|
519
|
+
ui.on("dstf_drawer_width", lambda e: self.resize(*e.args))
|
|
520
|
+
ui.run_javascript(
|
|
521
|
+
_WIDTH.replace("__DRAWER__", str(self.drawer.id))
|
|
522
|
+
.replace("__GRIP__", self.grip.html_id)
|
|
523
|
+
.replace("__MIN__", str(MIN_PANEL))
|
|
524
|
+
.replace("__MAX__", str(MAX_FRACTION))
|
|
525
|
+
.replace("__PLOTS__", str(MIN_PLOTS))
|
|
526
|
+
.replace("__RAIL__", str(RAIL))
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
def open(self, kind: str | None) -> None:
|
|
530
|
+
"""Show ``kind``'s panel and hide the other; None closes both. The
|
|
531
|
+
drawer widens to hold the panel, which is what narrows the plots.
|
|
532
|
+
The Variables drawer is built again only when it was not the one
|
|
533
|
+
open: an open one follows the page by `refresh`, and building it on
|
|
534
|
+
a select and again here built it twice."""
|
|
535
|
+
was = self.page.state.drawer
|
|
536
|
+
for name, panel in self.panels.items():
|
|
537
|
+
panel.set_visibility(name == kind)
|
|
538
|
+
for name, button in self.buttons.items():
|
|
539
|
+
button.classes(remove="dstf-rail-on")
|
|
540
|
+
if name == kind:
|
|
541
|
+
button.classes(add="dstf-rail-on")
|
|
542
|
+
self.page.state.drawer = kind
|
|
543
|
+
self._width(self.page.state.drawer_width)
|
|
544
|
+
if kind == "tests" and self.tests is None:
|
|
545
|
+
self.tests = TestsDrawer(self.page, self.panels["tests"])
|
|
546
|
+
elif kind == "variables":
|
|
547
|
+
if self.variables is None:
|
|
548
|
+
self.variables = VariablesDrawer(self.page, self.panels["variables"])
|
|
549
|
+
elif was != "variables":
|
|
550
|
+
self.variables.rebuild() # it was not followed while closed
|
|
551
|
+
|
|
552
|
+
def _width(self, width: int) -> None:
|
|
553
|
+
"""The drawer's `width` prop and the panels' width: RAIL alone with
|
|
554
|
+
no drawer open."""
|
|
555
|
+
kind = self.page.state.drawer
|
|
556
|
+
self.drawer.props(f"width={RAIL + width if kind else RAIL}")
|
|
557
|
+
for panel in self.panels.values():
|
|
558
|
+
panel.style(f"width: {width}px")
|
|
559
|
+
self.grip.props["aria-valuenow"] = str(width)
|
|
560
|
+
self.grip.set_visibility(kind is not None)
|
|
561
|
+
self.grip.update()
|
|
562
|
+
if kind is not None:
|
|
563
|
+
# The tab's width may be one a wider window held: the browser
|
|
564
|
+
# clamps it to this one's and sends it back (`fit`).
|
|
565
|
+
self.page.client.run_javascript(f"window.__dstfDrawer?.fit({width})")
|
|
566
|
+
|
|
567
|
+
def resize(self, width: Any, window: Any) -> None:
|
|
568
|
+
"""A drag's release (`_DRAG`): the width the browser clamped and the
|
|
569
|
+
window's width, held to the same bounds again here (`max_panel`,
|
|
570
|
+
then the floor), kept in `TabState`."""
|
|
571
|
+
try:
|
|
572
|
+
px = int(round(min(float(width), max_panel(float(window)))))
|
|
573
|
+
except (TypeError, ValueError, OverflowError):
|
|
574
|
+
return
|
|
575
|
+
px = max(MIN_PANEL, px)
|
|
576
|
+
self.page.state.drawer_width = px
|
|
577
|
+
self._width(px)
|
|
578
|
+
|
|
579
|
+
def toggle(self, kind: str) -> None:
|
|
580
|
+
self.open(None if self.page.state.drawer == kind else kind)
|
|
581
|
+
|
|
582
|
+
def refresh(self) -> None:
|
|
583
|
+
"""The open Variables drawer follows the page — the plot selected,
|
|
584
|
+
the layout staged, the result read — and builds again only when one
|
|
585
|
+
of those moved (`VariablesDrawer.refresh`). The ONE way the page's
|
|
586
|
+
changes reach it: `TestPage.select` and the end of `TestPage.sync`
|
|
587
|
+
call this. A closed one is left alone; it is rebuilt when it opens."""
|
|
588
|
+
if self.page.state.drawer == "variables" and self.variables is not None:
|
|
589
|
+
self.variables.refresh()
|
|
590
|
+
|
|
591
|
+
def handle(self, event: dict[str, Any]) -> None:
|
|
592
|
+
if self.tests is not None:
|
|
593
|
+
self.tests.handle(event)
|