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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. dstf/__init__.py +41 -0
  2. dstf/__main__.py +10 -0
  3. dstf/authoring/__init__.py +13 -0
  4. dstf/authoring/builtin/__init__.py +32 -0
  5. dstf/authoring/builtin/accept.py +205 -0
  6. dstf/authoring/builtin/edit_criteria.py +479 -0
  7. dstf/authoring/builtin/edit_family.py +228 -0
  8. dstf/authoring/builtin/edit_on_plot.py +1354 -0
  9. dstf/authoring/builtin/edit_tree.py +1399 -0
  10. dstf/authoring/builtin/enqueue.py +144 -0
  11. dstf/authoring/capabilities.py +463 -0
  12. dstf/authoring/errors.py +49 -0
  13. dstf/authoring/family_rows.py +526 -0
  14. dstf/authoring/forms.py +631 -0
  15. dstf/authoring/layout.py +104 -0
  16. dstf/authoring/live_rows.py +337 -0
  17. dstf/authoring/live_runs.py +385 -0
  18. dstf/authoring/needle.py +127 -0
  19. dstf/authoring/picker_rows.py +92 -0
  20. dstf/authoring/queue_rows.py +354 -0
  21. dstf/authoring/session.py +3016 -0
  22. dstf/authoring/shell/__init__.py +9 -0
  23. dstf/authoring/shell/actions.py +702 -0
  24. dstf/authoring/shell/app.py +53 -0
  25. dstf/authoring/shell/drawer_controls.py +190 -0
  26. dstf/authoring/shell/family_fold.py +468 -0
  27. dstf/authoring/shell/figure_hook.py +249 -0
  28. dstf/authoring/shell/grips.js +1000 -0
  29. dstf/authoring/shell/header.py +783 -0
  30. dstf/authoring/shell/live_view.py +93 -0
  31. dstf/authoring/shell/picker.py +267 -0
  32. dstf/authoring/shell/placing.py +122 -0
  33. dstf/authoring/shell/plot_card.py +1171 -0
  34. dstf/authoring/shell/queue_panel.py +433 -0
  35. dstf/authoring/shell/rail.py +593 -0
  36. dstf/authoring/shell/shell.py +528 -0
  37. dstf/authoring/shell/stats_card.py +501 -0
  38. dstf/authoring/shell/strip.py +204 -0
  39. dstf/authoring/shell/style.py +721 -0
  40. dstf/authoring/shell/suite.py +277 -0
  41. dstf/authoring/shell/suite_grid.js +414 -0
  42. dstf/authoring/shell/suite_grid.py +373 -0
  43. dstf/authoring/shell/tab_theme.py +110 -0
  44. dstf/authoring/shell/tabstate.py +146 -0
  45. dstf/authoring/shell/test_list.py +179 -0
  46. dstf/authoring/shell/test_page.py +1404 -0
  47. dstf/authoring/shell/tree_card.py +1171 -0
  48. dstf/authoring/shell/variables_drawer.py +589 -0
  49. dstf/authoring/shell/widgets.py +188 -0
  50. dstf/authoring/situation.py +287 -0
  51. dstf/authoring/spec_document.py +215 -0
  52. dstf/authoring/spec_preview.py +83 -0
  53. dstf/authoring/suite_rows.py +99 -0
  54. dstf/authoring/tree_move.py +516 -0
  55. dstf/authoring/tree_pointer.py +33 -0
  56. dstf/authoring/tree_text.py +61 -0
  57. dstf/authoring/typed_number.js +64 -0
  58. dstf/authoring/typed_number.py +76 -0
  59. dstf/authoring/unscorable.py +141 -0
  60. dstf/authoring/variable_rows.py +216 -0
  61. dstf/backends/__init__.py +283 -0
  62. dstf/backends/base.py +1117 -0
  63. dstf/backends/common/__init__.py +1 -0
  64. dstf/backends/common/json_driver.py +484 -0
  65. dstf/backends/common/live_mat.py +369 -0
  66. dstf/backends/common/mat_reader.py +632 -0
  67. dstf/backends/common/proc_kill.py +238 -0
  68. dstf/backends/common/proc_output.py +20 -0
  69. dstf/backends/common/result_files.py +109 -0
  70. dstf/backends/common/result_variables.py +329 -0
  71. dstf/backends/common/statistics.py +39 -0
  72. dstf/backends/cross_backend.py +193 -0
  73. dstf/backends/dymola/__init__.py +5 -0
  74. dstf/backends/dymola/backend.py +776 -0
  75. dstf/backends/dymola/interface_loader.py +241 -0
  76. dstf/backends/dymola/log_parser.py +396 -0
  77. dstf/backends/dymola/persistent_backend.py +949 -0
  78. dstf/backends/fmpy/__init__.py +11 -0
  79. dstf/backends/fmpy/backend.py +443 -0
  80. dstf/backends/julia/__init__.py +27 -0
  81. dstf/backends/julia/backend.py +156 -0
  82. dstf/backends/julia/persistent_backend.py +541 -0
  83. dstf/backends/julia/run_persistent.jl +167 -0
  84. dstf/backends/julia/run_test.jl +146 -0
  85. dstf/backends/openmodelica/__init__.py +21 -0
  86. dstf/backends/openmodelica/backend.py +368 -0
  87. dstf/backends/openmodelica/init_xml.py +76 -0
  88. dstf/backends/openmodelica/log_parser.py +119 -0
  89. dstf/backends/openmodelica/mos_generator.py +228 -0
  90. dstf/backends/openmodelica/persistent_backend.py +1045 -0
  91. dstf/backends/openmodelica/session_loader.py +63 -0
  92. dstf/backends/persistent.py +1811 -0
  93. dstf/backends/pool.py +602 -0
  94. dstf/backends/progress.py +395 -0
  95. dstf/backends/python/__init__.py +11 -0
  96. dstf/backends/python/backend.py +141 -0
  97. dstf/backends/python/run_test.py +141 -0
  98. dstf/backends/run_metadata.py +103 -0
  99. dstf/backends/types.py +310 -0
  100. dstf/cli/__init__.py +12 -0
  101. dstf/cli/author_cmd.py +61 -0
  102. dstf/cli/baseline_cmds.py +341 -0
  103. dstf/cli/common.py +231 -0
  104. dstf/cli/diag_cmds.py +350 -0
  105. dstf/cli/main.py +561 -0
  106. dstf/cli/review.py +398 -0
  107. dstf/cli/run_cmds.py +589 -0
  108. dstf/cli/spec_cmds.py +123 -0
  109. dstf/comparison/__init__.py +13 -0
  110. dstf/comparison/algorithms.py +2127 -0
  111. dstf/comparison/builtins.py +291 -0
  112. dstf/comparison/comparator.py +1189 -0
  113. dstf/comparison/geometry.py +164 -0
  114. dstf/comparison/handles.py +1194 -0
  115. dstf/comparison/metric_tree.py +332 -0
  116. dstf/comparison/mode_schema.py +176 -0
  117. dstf/comparison/modes.py +788 -0
  118. dstf/comparison/registry.py +230 -0
  119. dstf/comparison/seeds.py +281 -0
  120. dstf/comparison/sides.py +158 -0
  121. dstf/comparison/snap.py +62 -0
  122. dstf/comparison/strict_keys.py +246 -0
  123. dstf/comparison/summaries.py +354 -0
  124. dstf/comparison/tree_eval.py +868 -0
  125. dstf/comparison/tree_spec.py +678 -0
  126. dstf/comparison/types.py +194 -0
  127. dstf/comparison/validator.py +232 -0
  128. dstf/config.py +815 -0
  129. dstf/discovery/__init__.py +0 -0
  130. dstf/discovery/json_recognizer.py +702 -0
  131. dstf/discovery/library_state.py +248 -0
  132. dstf/discovery/mo_parser.py +794 -0
  133. dstf/discovery/patch_apply.py +308 -0
  134. dstf/discovery/patch_envelope.py +63 -0
  135. dstf/discovery/plots.py +318 -0
  136. dstf/discovery/recognizer.py +103 -0
  137. dstf/discovery/scan_cache.py +154 -0
  138. dstf/discovery/spec_parser.py +538 -0
  139. dstf/discovery/spec_shape.py +122 -0
  140. dstf/discovery/spec_update.py +493 -0
  141. dstf/discovery/test_registry.py +488 -0
  142. dstf/discovery/unit_tests.py +95 -0
  143. dstf/figures/__init__.py +14 -0
  144. dstf/figures/assemble.py +48 -0
  145. dstf/figures/decimate.py +150 -0
  146. dstf/figures/families.py +289 -0
  147. dstf/figures/palette.py +170 -0
  148. dstf/figures/plotly.py +1413 -0
  149. dstf/figures/view.py +233 -0
  150. dstf/pipeline.py +435 -0
  151. dstf/plugins.py +104 -0
  152. dstf/reporting/__init__.py +0 -0
  153. dstf/reporting/console_report.py +187 -0
  154. dstf/reporting/context_builder.py +979 -0
  155. dstf/reporting/dashboard_render.py +534 -0
  156. dstf/reporting/junit_report.py +192 -0
  157. dstf/reporting/overlay_loader.py +708 -0
  158. dstf/reporting/report_suite.py +1059 -0
  159. dstf/reporting/rows.py +458 -0
  160. dstf/reporting/schema_export.py +413 -0
  161. dstf/reporting/stats_rows.py +321 -0
  162. dstf/reporting/status.py +388 -0
  163. dstf/reporting/templates/axes.js +61 -0
  164. dstf/reporting/templates/dashboard.html +284 -0
  165. dstf/reporting/templates/dashboard.js +604 -0
  166. dstf/reporting/templates/filters.js +100 -0
  167. dstf/reporting/templates/interactive.html +420 -0
  168. dstf/reporting/templates/report.js +116 -0
  169. dstf/reporting/templates/vendor/plotly.min.js +8 -0
  170. dstf/reporting/theme.py +324 -0
  171. dstf/reporting/tree_rows.py +73 -0
  172. dstf/reporting/verdicts.py +124 -0
  173. dstf/storage/__init__.py +0 -0
  174. dstf/storage/accept.py +216 -0
  175. dstf/storage/baseline.py +43 -0
  176. dstf/storage/batch_manifest.py +204 -0
  177. dstf/storage/ref_index.py +295 -0
  178. dstf/storage/reference_store.py +661 -0
  179. dstf/storage/roles.py +576 -0
  180. dstf/storage/run_stats.py +85 -0
  181. dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
  182. dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
  183. dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
  184. dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
  185. dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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}"