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,433 @@
|
|
|
1
|
+
"""The simulation queue on the page (D150; brief §3.6; the `Main` and
|
|
2
|
+
`QueueIndicator` artboards): the header's indicator, on every page, and the
|
|
3
|
+
queue panel on the Suite page — one row per item in run order, drawn from
|
|
4
|
+
`authoring.queue_rows`. The panel also carries what the runs card it
|
|
5
|
+
replaced did beside the job: the store writes' lines and the artifacts.
|
|
6
|
+
|
|
7
|
+
Nothing here reads a file or a clock of its own: the rows are the Session's
|
|
8
|
+
queue as data, repainted on its `queue` events, and the shell's `poll` —
|
|
9
|
+
once a second while a test runs or a worker starts — moves only the words of the
|
|
10
|
+
rows still to end and the lines of the workers being started."""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
from urllib.parse import quote
|
|
18
|
+
|
|
19
|
+
from nicegui import ui
|
|
20
|
+
|
|
21
|
+
from ...backends.pool import ItemState
|
|
22
|
+
from ...reporting.status import STATUS_CLASSES, STATUS_LABELS
|
|
23
|
+
from ..capabilities import Capability, Placement
|
|
24
|
+
from ..errors import user_text
|
|
25
|
+
from ..queue_rows import (
|
|
26
|
+
CANCEL,
|
|
27
|
+
OPEN,
|
|
28
|
+
READING,
|
|
29
|
+
REMOVE,
|
|
30
|
+
RESTARTING,
|
|
31
|
+
QueueRow,
|
|
32
|
+
counts_text,
|
|
33
|
+
indicator,
|
|
34
|
+
queue_rows,
|
|
35
|
+
starting_texts,
|
|
36
|
+
)
|
|
37
|
+
from ..session import Selection, StaleSpecError
|
|
38
|
+
from ..typed_number import TypedNumber
|
|
39
|
+
from .actions import ActionBar, log_unless_users
|
|
40
|
+
from .widgets import Action, IconAction
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from .shell import Shell
|
|
44
|
+
|
|
45
|
+
#: The panel's anchor: the header's indicator links to it.
|
|
46
|
+
ANCHOR = "queue"
|
|
47
|
+
|
|
48
|
+
QUEUE_CSS = """
|
|
49
|
+
/* It gives way only past the counts' width: a stopped pool's message is
|
|
50
|
+
cut with an ellipsis, "1 running · 2 queued" never is (it was, at 1024). */
|
|
51
|
+
.dstf-indicator { font-size: 13px; color: var(--dstf-text-secondary); text-decoration: none;
|
|
52
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto;
|
|
53
|
+
min-width: 10.5em; text-align: center; }
|
|
54
|
+
.dstf-indicator:hover { text-decoration: underline; }
|
|
55
|
+
.dstf-indicator-running { color: var(--dstf-running-dark); font-weight: 600; }
|
|
56
|
+
.dstf-indicator-failed { color: var(--dstf-fail-dark); font-weight: 600; }
|
|
57
|
+
/* The table and the panel side by side (the Main artboard); under 1200 px,
|
|
58
|
+
and while the queue is empty, the panel is a line above the table. */
|
|
59
|
+
.dstf-suite-body { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px;
|
|
60
|
+
width: 100%; }
|
|
61
|
+
.dstf-suite-main { flex: 1 1 0; min-width: 0; }
|
|
62
|
+
.dstf-queue.dstf-card { flex: 0 0 420px; min-width: 0; padding: 8px 0; scroll-margin-top: 64px; }
|
|
63
|
+
.dstf-queue.dstf-queue-empty { flex: 1 1 100%; order: -1; padding: 2px 0; }
|
|
64
|
+
@media (max-width: 1199px) { .dstf-queue.dstf-card { flex: 1 1 100%; order: -1; } }
|
|
65
|
+
.dstf-queue-head { padding: 0 12px; row-gap: 4px; }
|
|
66
|
+
.dstf-queue-title { font-weight: 600; }
|
|
67
|
+
.dstf-queue-workers { width: 76px; }
|
|
68
|
+
.dstf-queue-row { padding: 4px 12px; border-top: 1px solid var(--dstf-border-hairline); }
|
|
69
|
+
/* Above the table every row is a row the table sits lower by: there the
|
|
70
|
+
rows scroll inside about a third of the window (fifteen uncleared ones
|
|
71
|
+
put the table below the fold). Beside it the panel pushes nothing. */
|
|
72
|
+
@media (max-width: 1199px) { .dstf-queue-rows { max-height: 34vh; overflow-y: auto; } }
|
|
73
|
+
.dstf-queue-num { width: 20px; text-align: right; flex: none; font-family: var(--dstf-mono);
|
|
74
|
+
color: var(--dstf-text-muted); }
|
|
75
|
+
.dstf-queue-what { flex: 1 1 0; min-width: 0; }
|
|
76
|
+
.dstf-queue-what > * { max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
77
|
+
white-space: nowrap; }
|
|
78
|
+
.dstf-queue-words { font-size: 12px; color: var(--dstf-text-secondary); }
|
|
79
|
+
.dstf-queue-starting > * { padding: 2px 12px; overflow-wrap: anywhere; }
|
|
80
|
+
.dstf-queue-acts { flex: none; }
|
|
81
|
+
.dstf-queue-error { margin: 0 12px; padding: 4px 8px; border-radius: 4px; font-size: 12px;
|
|
82
|
+
color: var(--dstf-fail-dark); background: var(--dstf-fail-tint);
|
|
83
|
+
border: 1px solid var(--dstf-fail-border); overflow-wrap: anywhere; }
|
|
84
|
+
.dstf-queue-notes { padding: 4px 12px 0; overflow-wrap: anywhere; }
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
WORKERS_HINT = (
|
|
88
|
+
"How many tests run at once. A change applies to workers started after "
|
|
89
|
+
"it: a running test keeps its worker."
|
|
90
|
+
)
|
|
91
|
+
STOP_WORKERS_HINT = (
|
|
92
|
+
"Ends the simulator processes and frees their licence seats; the next "
|
|
93
|
+
"queued test starts new ones."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class QueueIndicator:
|
|
98
|
+
"""The header's queue indicator (every page): `3 running · 5 queued`,
|
|
99
|
+
`idle`, or — red — the first words of the message the pool stopped
|
|
100
|
+
with. A click opens the Suite page at the queue panel."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, shell: Shell) -> None:
|
|
103
|
+
self.shell = shell
|
|
104
|
+
ui.add_css(QUEUE_CSS)
|
|
105
|
+
self.link = (
|
|
106
|
+
ui.link("", f"/#{ANCHOR}").classes("dstf-indicator").mark("queue-indicator")
|
|
107
|
+
)
|
|
108
|
+
with self.link:
|
|
109
|
+
self.tip = ui.tooltip("").mark("queue-indicator:tip")
|
|
110
|
+
self.refresh()
|
|
111
|
+
|
|
112
|
+
def refresh(self) -> None:
|
|
113
|
+
state = indicator(self.shell.session)
|
|
114
|
+
self.link.set_text(state.text)
|
|
115
|
+
self.link.classes(
|
|
116
|
+
remove="dstf-indicator-idle dstf-indicator-running dstf-indicator-failed",
|
|
117
|
+
add=f"dstf-indicator-{state.tone}",
|
|
118
|
+
)
|
|
119
|
+
self.tip.set_text(state.tip)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class QueuePanel:
|
|
123
|
+
def __init__(
|
|
124
|
+
self, shell: Shell, stale: Callable[[StaleSpecError], None] | None = None
|
|
125
|
+
) -> None:
|
|
126
|
+
self.shell = shell
|
|
127
|
+
#: The test a row's capability button was clicked on: the selection
|
|
128
|
+
#: the panel's one dialog acts over.
|
|
129
|
+
self.row_model: str | None = None
|
|
130
|
+
#: The words of the rows still to end — waiting or running — and
|
|
131
|
+
#: the state each was drawn in, repainted by `tick`.
|
|
132
|
+
self._moving: dict[str, tuple[ItemState, ui.label]] = {}
|
|
133
|
+
#: One line per worker being started (`_paint_starting`).
|
|
134
|
+
self._starting: list[ui.label] = []
|
|
135
|
+
cores = os.cpu_count() or 1
|
|
136
|
+
with (
|
|
137
|
+
ui.column()
|
|
138
|
+
.classes("dstf-card dstf-queue gap-1")
|
|
139
|
+
.props(f"id={ANCHOR}")
|
|
140
|
+
.mark("queue") as self.card
|
|
141
|
+
):
|
|
142
|
+
with ui.row().classes("dstf-queue-head items-center gap-2 w-full"):
|
|
143
|
+
ui.label("Simulation queue").classes("dstf-queue-title")
|
|
144
|
+
self.counts = ui.label().classes("dstf-why").mark("queue-counts")
|
|
145
|
+
ui.space()
|
|
146
|
+
self.workers = (
|
|
147
|
+
TypedNumber(
|
|
148
|
+
label="workers",
|
|
149
|
+
min=1,
|
|
150
|
+
max=cores,
|
|
151
|
+
step=1,
|
|
152
|
+
format="%d",
|
|
153
|
+
on_change=self._set_workers,
|
|
154
|
+
)
|
|
155
|
+
.props("dense outlined")
|
|
156
|
+
.classes("dstf-queue-workers")
|
|
157
|
+
.mark("queue-workers")
|
|
158
|
+
)
|
|
159
|
+
with self.workers:
|
|
160
|
+
ui.tooltip(WORKERS_HINT)
|
|
161
|
+
self.clear = (
|
|
162
|
+
ui.button("Clear finished", on_click=self._clear)
|
|
163
|
+
.props("flat dense no-caps")
|
|
164
|
+
.mark("queue-clear")
|
|
165
|
+
)
|
|
166
|
+
more = (
|
|
167
|
+
ui.button(icon="more_vert")
|
|
168
|
+
.props('flat dense round aria-label="Queue actions"')
|
|
169
|
+
.props('aria-haspopup="menu"')
|
|
170
|
+
.mark("queue-more")
|
|
171
|
+
)
|
|
172
|
+
with (
|
|
173
|
+
more,
|
|
174
|
+
ui.menu(),
|
|
175
|
+
ui.menu_item("Stop workers", on_click=self._stop_workers)
|
|
176
|
+
.props('role="menuitem"')
|
|
177
|
+
.mark("queue-stop-workers"),
|
|
178
|
+
):
|
|
179
|
+
ui.tooltip(STOP_WORKERS_HINT)
|
|
180
|
+
self.error = ui.label().classes("dstf-queue-error").mark("queue-error")
|
|
181
|
+
# The workers being brought up, one line each: a library's
|
|
182
|
+
# load is minutes, and its tests wait meanwhile.
|
|
183
|
+
self.starting = (
|
|
184
|
+
ui.column().classes("dstf-queue-starting w-full gap-0")
|
|
185
|
+
).mark("queue-starting")
|
|
186
|
+
self.rows = (
|
|
187
|
+
ui.column().classes("dstf-queue-rows w-full gap-0").mark("queue-rows")
|
|
188
|
+
)
|
|
189
|
+
self.notes = ui.column().classes("dstf-queue-notes w-full gap-1")
|
|
190
|
+
# The rows' capability buttons open ONE dialog, the panel's — a
|
|
191
|
+
# row's own would go with the row at the next repaint. The bar
|
|
192
|
+
# draws no button of its own.
|
|
193
|
+
self.bar = ActionBar(
|
|
194
|
+
shell,
|
|
195
|
+
Placement.SUITE,
|
|
196
|
+
lambda: Selection(
|
|
197
|
+
tests=frozenset({self.row_model} if self.row_model else ())
|
|
198
|
+
),
|
|
199
|
+
marker="queue-row-actions",
|
|
200
|
+
stale=stale,
|
|
201
|
+
)
|
|
202
|
+
self.bar.clear()
|
|
203
|
+
self.bar.container.set_visibility(False)
|
|
204
|
+
self.refresh()
|
|
205
|
+
|
|
206
|
+
# -- the Session's events --------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def handle(self, event: dict[str, Any]) -> None:
|
|
209
|
+
kind = event["event"]
|
|
210
|
+
if kind == "poll":
|
|
211
|
+
self.tick()
|
|
212
|
+
elif kind in ("queue", "notice", "results", "spec", "opened"):
|
|
213
|
+
# `results`: a finished row's badge is its test's verdict, and
|
|
214
|
+
# an edit rescored it. `spec`: an open or a close resets the
|
|
215
|
+
# queue and the notices.
|
|
216
|
+
self.refresh()
|
|
217
|
+
|
|
218
|
+
def tick(self) -> None:
|
|
219
|
+
"""The words of the rows still to end — a running row's seconds or
|
|
220
|
+
how far its result file has got (D151), a waiting row's "worker
|
|
221
|
+
starting" — and the starting workers' lines; nothing else moved."""
|
|
222
|
+
rows = {r.model_id: r for r in queue_rows(self.shell.session)}
|
|
223
|
+
for model_id, (state, label) in self._moving.items():
|
|
224
|
+
row = rows.get(model_id)
|
|
225
|
+
if row is not None and row.state is state:
|
|
226
|
+
label.set_text(_words(row))
|
|
227
|
+
self._paint_starting()
|
|
228
|
+
|
|
229
|
+
def _paint_starting(self) -> None:
|
|
230
|
+
texts = starting_texts(self.shell.session)
|
|
231
|
+
if len(self._starting) != len(texts):
|
|
232
|
+
self.starting.clear()
|
|
233
|
+
with self.starting:
|
|
234
|
+
self._starting = [
|
|
235
|
+
ui.label().classes("dstf-queue-words").mark("queue-starting-worker")
|
|
236
|
+
for _ in texts
|
|
237
|
+
]
|
|
238
|
+
for label, text in zip(self._starting, texts):
|
|
239
|
+
label.set_text(text)
|
|
240
|
+
self.starting.set_visibility(bool(texts))
|
|
241
|
+
|
|
242
|
+
def refresh(self) -> None:
|
|
243
|
+
session = self.shell.session
|
|
244
|
+
rows = queue_rows(session)
|
|
245
|
+
stopped = session.queue_stopped
|
|
246
|
+
empty = not rows and stopped is None
|
|
247
|
+
self.card.classes(
|
|
248
|
+
remove="dstf-queue-empty", add="dstf-queue-empty" if empty else ""
|
|
249
|
+
)
|
|
250
|
+
self.counts.set_text(counts_text(rows))
|
|
251
|
+
project = session.project
|
|
252
|
+
self.workers.set_enabled(project is not None)
|
|
253
|
+
if project is not None and self.workers.value != session.workers:
|
|
254
|
+
self.workers.set_value(session.workers)
|
|
255
|
+
ended = any(r.state not in (ItemState.WAITING, ItemState.RUNNING) for r in rows)
|
|
256
|
+
self.clear.set_visibility(ended or stopped is not None)
|
|
257
|
+
self.error.set_text(stopped or "")
|
|
258
|
+
self.error.set_visibility(stopped is not None)
|
|
259
|
+
self._paint_starting()
|
|
260
|
+
self.rows.clear()
|
|
261
|
+
self._moving = {}
|
|
262
|
+
with self.rows:
|
|
263
|
+
for row in rows:
|
|
264
|
+
self._row(row)
|
|
265
|
+
self.notes.clear()
|
|
266
|
+
with self.notes:
|
|
267
|
+
for notice in session.notices:
|
|
268
|
+
ui.label(notice).classes("text-xs").mark("notice")
|
|
269
|
+
for artifact in session.artifacts:
|
|
270
|
+
for path in artifact.paths:
|
|
271
|
+
ui.link(
|
|
272
|
+
f"{artifact.label}: {path.name}", path.as_uri(), new_tab=True
|
|
273
|
+
)
|
|
274
|
+
self.notes.set_visibility(bool(session.notices or session.artifacts))
|
|
275
|
+
|
|
276
|
+
# -- one row -----------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def _row(self, row: QueueRow) -> None:
|
|
279
|
+
model_id = row.model_id
|
|
280
|
+
# An earlier run's row of a test that ran again wears no verdict
|
|
281
|
+
# (`QueueRow.status`): no tint, no badge.
|
|
282
|
+
css = STATUS_CLASSES[row.status] if row.status is not None else None
|
|
283
|
+
tint = f" dstf-tint-{css}" if css is not None else ""
|
|
284
|
+
with (
|
|
285
|
+
ui.row()
|
|
286
|
+
.classes(f"dstf-queue-row{tint} items-center gap-2 w-full no-wrap")
|
|
287
|
+
# The row's test, for a script that reads the page.
|
|
288
|
+
.props(f'data-model="{model_id}"')
|
|
289
|
+
.mark(f"queue-row:{model_id}")
|
|
290
|
+
):
|
|
291
|
+
ui.label(str(row.number)).classes("dstf-queue-num")
|
|
292
|
+
if row.status is not None:
|
|
293
|
+
ui.badge(STATUS_LABELS[row.status], color=None).classes(
|
|
294
|
+
"dstf-badge"
|
|
295
|
+
).style(f"background-color: var(--dstf-{css}-fill)").mark(
|
|
296
|
+
f"queue-status:{model_id}"
|
|
297
|
+
)
|
|
298
|
+
with ui.column().classes("dstf-queue-what gap-0"):
|
|
299
|
+
with ui.link(
|
|
300
|
+
model_id.rpartition(".")[2], f"/test/{quote(model_id, safe='')}"
|
|
301
|
+
).mark(f"queue-test:{model_id}"):
|
|
302
|
+
ui.tooltip(model_id)
|
|
303
|
+
words = (
|
|
304
|
+
ui.label(_words(row))
|
|
305
|
+
.classes("dstf-queue-words")
|
|
306
|
+
.mark(f"queue-words:{model_id}")
|
|
307
|
+
)
|
|
308
|
+
if row.message:
|
|
309
|
+
# The backend's message in full; the row says its first line.
|
|
310
|
+
with words:
|
|
311
|
+
ui.tooltip(row.message).mark(f"queue-words:{model_id}:tip")
|
|
312
|
+
if row.state in (ItemState.WAITING, ItemState.RUNNING):
|
|
313
|
+
self._moving[model_id] = (row.state, words)
|
|
314
|
+
with ui.row().classes("dstf-queue-acts items-center gap-1 no-wrap"):
|
|
315
|
+
self._actions(row)
|
|
316
|
+
|
|
317
|
+
def _actions(self, row: QueueRow) -> None:
|
|
318
|
+
model_id = row.model_id
|
|
319
|
+
if not row.actions and row.cancel_why is None:
|
|
320
|
+
return # a test whose click is still being read: no item yet
|
|
321
|
+
item = self.shell.session.queue[row.number - 1]
|
|
322
|
+
if REMOVE in row.actions:
|
|
323
|
+
with (
|
|
324
|
+
ui.button("Remove", on_click=lambda: self._do(self._remove, item))
|
|
325
|
+
.props("flat dense no-caps")
|
|
326
|
+
.mark(f"queue-remove:{model_id}")
|
|
327
|
+
):
|
|
328
|
+
ui.tooltip("Takes it out of the queue before it starts.")
|
|
329
|
+
if CANCEL in row.actions:
|
|
330
|
+
with (
|
|
331
|
+
ui.button("Cancel", on_click=lambda: self._do(self._cancel, item))
|
|
332
|
+
.props("flat dense no-caps color=negative")
|
|
333
|
+
.mark(f"queue-cancel:{model_id}")
|
|
334
|
+
):
|
|
335
|
+
ui.tooltip("Stops this simulation. It leaves no result.")
|
|
336
|
+
elif row.cancel_why is not None:
|
|
337
|
+
# No Cancel where the backend cannot stop a test: the sentence
|
|
338
|
+
# says why, in the button's place.
|
|
339
|
+
with ui.icon("info").classes("text-secondary"):
|
|
340
|
+
ui.tooltip(row.cancel_why).mark(f"queue-cancel:{model_id}:why")
|
|
341
|
+
if OPEN not in row.actions:
|
|
342
|
+
return
|
|
343
|
+
url = f"/test/{quote(model_id, safe='')}"
|
|
344
|
+
ui.button("Open test", on_click=lambda: ui.navigate.to(url)).props(
|
|
345
|
+
"flat dense no-caps"
|
|
346
|
+
).mark(f"queue-open:{model_id}")
|
|
347
|
+
if not row.latest:
|
|
348
|
+
# An earlier run of a test that ran again: what acts on the
|
|
349
|
+
# TEST is on its latest row (`QueueRow.latest`).
|
|
350
|
+
return
|
|
351
|
+
# What the Suite's batch bar offers, for this one test: Accept on a
|
|
352
|
+
# finished row, Queue again on any. The panel knows neither by name.
|
|
353
|
+
view = self.shell.view_for(Selection(tests=frozenset({model_id})))
|
|
354
|
+
for cap, avail in self.shell.placed(Placement.SUITE, view):
|
|
355
|
+
try:
|
|
356
|
+
caption = cap.caption(view)
|
|
357
|
+
except Exception as e: # the label is the fallback; the row draws
|
|
358
|
+
caption = cap.label
|
|
359
|
+
self.shell.report_once(cap.name, "caption", e)
|
|
360
|
+
reason = (
|
|
361
|
+
None
|
|
362
|
+
if avail is True
|
|
363
|
+
else (avail if isinstance(avail, str) else "not available")
|
|
364
|
+
)
|
|
365
|
+
marker = f"queue-{cap.name}:{model_id}"
|
|
366
|
+
icon = getattr(cap, "icon", None)
|
|
367
|
+
action: Action | IconAction
|
|
368
|
+
if icon is not None:
|
|
369
|
+
action = IconAction(
|
|
370
|
+
self._opener(cap, model_id), icon=icon, marker=marker
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
action = Action(
|
|
374
|
+
self._opener(cap, model_id),
|
|
375
|
+
props="flat dense no-caps",
|
|
376
|
+
marker=marker,
|
|
377
|
+
why=False,
|
|
378
|
+
)
|
|
379
|
+
action.set(caption, reason)
|
|
380
|
+
|
|
381
|
+
def _opener(self, cap: Capability, model_id: str) -> Callable[[], None]:
|
|
382
|
+
def open_dialog() -> None:
|
|
383
|
+
self.row_model = model_id
|
|
384
|
+
self.bar.open(cap)
|
|
385
|
+
|
|
386
|
+
return open_dialog
|
|
387
|
+
|
|
388
|
+
# -- the panel's own actions -------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def _do(self, act: Callable[[Any], None], item: Any) -> None:
|
|
391
|
+
"""A row's Remove or Cancel; a refusal is said, never raised."""
|
|
392
|
+
try:
|
|
393
|
+
act(item)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
log_unless_users(e, "queue")
|
|
396
|
+
ui.notify(user_text(e), type="warning")
|
|
397
|
+
|
|
398
|
+
def _remove(self, item: Any) -> None:
|
|
399
|
+
if not self.shell.session.remove(item):
|
|
400
|
+
ui.notify(f"{item.model_id} has already started", type="warning")
|
|
401
|
+
|
|
402
|
+
def _cancel(self, item: Any) -> None:
|
|
403
|
+
if not self.shell.session.cancel(item):
|
|
404
|
+
ui.notify(f"{item.model_id} is no longer running", type="warning")
|
|
405
|
+
|
|
406
|
+
def _set_workers(self, e: Any) -> None:
|
|
407
|
+
session = self.shell.session
|
|
408
|
+
if e.value is None or session.project is None:
|
|
409
|
+
return
|
|
410
|
+
count = int(e.value)
|
|
411
|
+
if count != session.workers:
|
|
412
|
+
session.set_workers(count)
|
|
413
|
+
|
|
414
|
+
def _clear(self) -> None:
|
|
415
|
+
self.shell.session.clear_finished()
|
|
416
|
+
|
|
417
|
+
def _stop_workers(self) -> None:
|
|
418
|
+
try:
|
|
419
|
+
self.shell.session.stop_workers()
|
|
420
|
+
except Exception as e:
|
|
421
|
+
log_unless_users(e, "stop workers")
|
|
422
|
+
ui.notify(user_text(e), type="warning")
|
|
423
|
+
return
|
|
424
|
+
ui.notify("workers stopped")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _words(row: QueueRow) -> str:
|
|
428
|
+
"""The row's state line: its words, and its worker while it runs — the
|
|
429
|
+
worker's line above says which while it is being brought back, or
|
|
430
|
+
reads the library again."""
|
|
431
|
+
if row.worker is None or row.progress in (RESTARTING, READING):
|
|
432
|
+
return row.words
|
|
433
|
+
return f"{row.words} · worker {row.worker}"
|