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,354 @@
|
|
|
1
|
+
"""The simulation queue as data (D150): one :class:`QueueRow` per item the
|
|
2
|
+
Session's queue lists, in run order, and the header's indicator — the words
|
|
3
|
+
each state is said in, the status its badge wears, what its row offers. The
|
|
4
|
+
queue panel, the header and the Test page's strip draw from these; no UI
|
|
5
|
+
framework here (the Session tier), and no status word of its own
|
|
6
|
+
(``reporting.status``)."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from ..backends.pool import ItemState
|
|
17
|
+
from ..reporting.status import CANCELLED, NOT_RUN, QUEUED, RUNNING, SIM_FAIL, classify
|
|
18
|
+
from .capabilities import plural
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..backends.pool import QueueItem, StartingWorker
|
|
22
|
+
from .session import Session
|
|
23
|
+
|
|
24
|
+
#: What a row may offer, by the name the panel draws it under.
|
|
25
|
+
REMOVE, CANCEL, OPEN = "remove", "cancel", "open"
|
|
26
|
+
|
|
27
|
+
#: A running row a stop was asked of, until it ends.
|
|
28
|
+
CANCELLING = "cancelling…"
|
|
29
|
+
|
|
30
|
+
#: How much of the stopped pool's message the header shows; the rest is its
|
|
31
|
+
#: tooltip.
|
|
32
|
+
INDICATOR_WIDTH = 48
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class QueueRow:
|
|
37
|
+
"""One item of the queue, as its row says it."""
|
|
38
|
+
|
|
39
|
+
#: Its place in the queue's list, from 1: the run order (first in,
|
|
40
|
+
#: first out).
|
|
41
|
+
number: int
|
|
42
|
+
model_id: str
|
|
43
|
+
state: ItemState
|
|
44
|
+
#: The state in words: ``waiting · 3 ahead`` (``waiting · next`` with
|
|
45
|
+
#: none, ``waiting · worker starting`` while no worker is up, ``waiting
|
|
46
|
+
#: · checking the library`` while its click's read is out and the queue
|
|
47
|
+
#: does not hold it yet),
|
|
48
|
+
#: ``running · 42 s`` (``running · t = 42 / 100 s`` where the backend
|
|
49
|
+
#: has a live view, D151; ``running · worker restarting`` while its
|
|
50
|
+
#: worker is brought back after a death, ``running · reading the
|
|
51
|
+
#: library`` while its worker reads files that changed since it read
|
|
52
|
+
#: them),
|
|
53
|
+
#: ``cancelling…`` once a stop was asked of a running one and until it
|
|
54
|
+
#: ends, ``finished · 9 s``, ``failed: <the backend's message, one
|
|
55
|
+
#: line>``, ``cancelled``.
|
|
56
|
+
words: str
|
|
57
|
+
#: The badge: QUEUED / RUNNING while the queue holds it, the test's
|
|
58
|
+
#: verdict once it finished, SIM_FAIL for a run that failed, CANCELLED,
|
|
59
|
+
#: and NOT RUN for an item no worker could start. None for a finished
|
|
60
|
+
#: row that is not its test's `latest`: a verdict is the TEST's, as it
|
|
61
|
+
#: stands now, and one test has one — on the row of its last run. Two
|
|
62
|
+
#: rows of one test both read the later run's verdict, the earlier
|
|
63
|
+
#: one beside the words of a run that had ended otherwise.
|
|
64
|
+
status: str | None
|
|
65
|
+
#: The worker it runs on, from 1, while it runs.
|
|
66
|
+
worker: int | None = None
|
|
67
|
+
#: A running item a stop was asked of (`QueueItem.stopping`): it
|
|
68
|
+
#: offers no second Cancel, and says so until it ends.
|
|
69
|
+
cancelling: bool = False
|
|
70
|
+
#: The items ahead of a waiting one: running, or waiting before it.
|
|
71
|
+
ahead: int = 0
|
|
72
|
+
#: Seconds since it started while it runs; its run's length once ended.
|
|
73
|
+
elapsed: float | None = None
|
|
74
|
+
#: How far a running item's result file has got (``t = 42 / 100 s``,
|
|
75
|
+
#: ``starting`` before its first row), or ``worker restarting``, or
|
|
76
|
+
#: ``reading the library``; None on a backend with no live view, and
|
|
77
|
+
#: for an item that is not running.
|
|
78
|
+
progress: str | None = None
|
|
79
|
+
#: The backend's message for a failed item, in full.
|
|
80
|
+
message: str | None = None
|
|
81
|
+
#: Whether a worker ever took it: False for a waiting item and for one
|
|
82
|
+
#: the pool failed because no worker could start.
|
|
83
|
+
started: bool = False
|
|
84
|
+
#: ``remove`` while it waits, ``cancel`` while it runs on a backend that
|
|
85
|
+
#: can stop a test, ``open`` (its Test page) once it has ended; none
|
|
86
|
+
#: while its click is being read — there is no item to act on yet.
|
|
87
|
+
actions: tuple[str, ...] = ()
|
|
88
|
+
#: Whether no later item of its test is listed. An ended row that is
|
|
89
|
+
#: not the latest says how its own run ended (``finished · 3 s``) and
|
|
90
|
+
#: offers its Test page, nothing else: what acts on the TEST — Accept,
|
|
91
|
+
#: Queue again — acts on the test's latest results, and under an
|
|
92
|
+
#: earlier row's name that reads as accepting that run's.
|
|
93
|
+
latest: bool = True
|
|
94
|
+
#: Why a running item has no ``cancel``: the backend's sentence.
|
|
95
|
+
cancel_why: str | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#: Where Python's traceback starts in a message.
|
|
99
|
+
_TRACEBACK = "Traceback (most recent call last):"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def one_line(text: str) -> str:
|
|
103
|
+
"""``text`` as one line: its first line that says something — and, for
|
|
104
|
+
a message that carries a Python traceback, what was raised (the
|
|
105
|
+
traceback's LAST line) after whatever the message said before it. The
|
|
106
|
+
first line alone read "Python execution failed: Traceback (most recent
|
|
107
|
+
call last):" for every failing `simulate`."""
|
|
108
|
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
109
|
+
if not lines:
|
|
110
|
+
return ""
|
|
111
|
+
head, found, _ = lines[0].partition(_TRACEBACK)
|
|
112
|
+
if found and len(lines) > 1:
|
|
113
|
+
return f"{head.strip()} {lines[-1]}".strip()
|
|
114
|
+
return lines[0]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
#: A quoted absolute path in a backend's phase: all but its last part is
|
|
118
|
+
#: cut (``loadFile("…/package.mo")``).
|
|
119
|
+
_LONG_PATH = re.compile(r'"(?:[A-Za-z]:)?[/\\][^"]*[/\\]([^"/\\]+)"')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
#: A running row whose worker died and is being brought back.
|
|
123
|
+
RESTARTING = "worker restarting"
|
|
124
|
+
|
|
125
|
+
#: A running row whose worker is up and reads the library again before the
|
|
126
|
+
#: test: its files changed since the worker read them, and a large library
|
|
127
|
+
#: is as long to read again as it was to load. The ONE phrase for it — the
|
|
128
|
+
#: row's words, the strip's sentence and the worker's line all say this.
|
|
129
|
+
READING = "reading the library"
|
|
130
|
+
|
|
131
|
+
#: A row of a test a Queue click named, while that click's read of the
|
|
132
|
+
#: library is still out (`Session.checking`): the tool has taken the test
|
|
133
|
+
#: and is looking at the files — a stat of each, and the test's own file
|
|
134
|
+
#: read again where it changed. It has no place in the queue yet, and the
|
|
135
|
+
#: read may still refuse it (its file is gone). The ONE phrase for it.
|
|
136
|
+
CHECKING = "checking the library"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def starting_text(worker: StartingWorker) -> str:
|
|
140
|
+
"""One worker that is not ready, as the queue panel lists it:
|
|
141
|
+
``starting worker 1 — loadFile("…/package.mo") · 85 s``, the phase in
|
|
142
|
+
the backend's own words with a path cut to its last part; ``starting
|
|
143
|
+
worker 1 · 85 s`` where the backend names no phase; ``restarting worker
|
|
144
|
+
1 — …`` for one brought back after a death; ``worker 1 reading the
|
|
145
|
+
library — …`` for one that is up and reads changed files again."""
|
|
146
|
+
phase = _LONG_PATH.sub(r'"…/\1"', worker.phase)
|
|
147
|
+
doing = f" — {phase}" if phase else ""
|
|
148
|
+
number = worker.worker_id + 1
|
|
149
|
+
if worker.reload:
|
|
150
|
+
return f"worker {number} {READING}{doing} · {worker.seconds:.0f} s"
|
|
151
|
+
verb = "restarting" if worker.restart else "starting"
|
|
152
|
+
return f"{verb} worker {number}{doing} · {worker.seconds:.0f} s"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def starting_texts(session: Session) -> list[str]:
|
|
156
|
+
"""The queue's workers being brought up right now, by worker."""
|
|
157
|
+
return [starting_text(worker) for worker in session.starting_workers]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _row(
|
|
161
|
+
session: Session,
|
|
162
|
+
item: QueueItem,
|
|
163
|
+
number: int,
|
|
164
|
+
ahead: int,
|
|
165
|
+
now: float,
|
|
166
|
+
starting: Mapping[int, StartingWorker],
|
|
167
|
+
latest: bool,
|
|
168
|
+
) -> QueueRow:
|
|
169
|
+
state = session.item_state(item)
|
|
170
|
+
started = item.started_at is not None
|
|
171
|
+
fields: dict[str, Any] = {}
|
|
172
|
+
if state is ItemState.WAITING:
|
|
173
|
+
if ahead:
|
|
174
|
+
words = f"waiting · {ahead} ahead"
|
|
175
|
+
else:
|
|
176
|
+
# Nothing ahead and it has not started: no worker is up. One
|
|
177
|
+
# being brought up says so — a library's load is minutes, and
|
|
178
|
+
# "next" for that long reads as a hang.
|
|
179
|
+
words = "waiting · worker starting" if starting else "waiting · next"
|
|
180
|
+
fields = {
|
|
181
|
+
"words": words,
|
|
182
|
+
"status": QUEUED,
|
|
183
|
+
"ahead": ahead,
|
|
184
|
+
"actions": (REMOVE,),
|
|
185
|
+
}
|
|
186
|
+
# Failed by the pool and not yet taken in: no worker took it.
|
|
187
|
+
started = False
|
|
188
|
+
elif state is ItemState.RUNNING:
|
|
189
|
+
elapsed = max(0.0, now - (item.started_at or now))
|
|
190
|
+
why = session.cannot_cancel
|
|
191
|
+
# Its worker died and is being brought back, or is reading the
|
|
192
|
+
# library again: the test has not begun, whatever its file or its
|
|
193
|
+
# clock would say.
|
|
194
|
+
busy = starting.get(item.worker_id) if item.worker_id is not None else None
|
|
195
|
+
live = (
|
|
196
|
+
session.live_runs.progress(item)
|
|
197
|
+
if busy is None
|
|
198
|
+
else (READING if busy.reload else RESTARTING)
|
|
199
|
+
)
|
|
200
|
+
fields = {
|
|
201
|
+
"words": f"running · {live or f'{elapsed:.0f} s'}",
|
|
202
|
+
"progress": live,
|
|
203
|
+
"status": RUNNING,
|
|
204
|
+
"worker": None if item.worker_id is None else item.worker_id + 1,
|
|
205
|
+
"elapsed": elapsed,
|
|
206
|
+
"actions": () if why is not None else (CANCEL,),
|
|
207
|
+
"cancel_why": why,
|
|
208
|
+
}
|
|
209
|
+
if item.stopping:
|
|
210
|
+
# Cancel was taken and the simulation has not ended: one that
|
|
211
|
+
# does not look at the request runs on for its worker's grace
|
|
212
|
+
# (Dymola, a model blocked in an external call: about 11 s),
|
|
213
|
+
# and `running · 42 s` beside a Cancel button said the click
|
|
214
|
+
# went nowhere.
|
|
215
|
+
fields.update(words=CANCELLING, cancelling=True, actions=())
|
|
216
|
+
else:
|
|
217
|
+
ran = (
|
|
218
|
+
item.ended_at - item.started_at
|
|
219
|
+
if item.started_at is not None and item.ended_at is not None
|
|
220
|
+
else None
|
|
221
|
+
)
|
|
222
|
+
fields = {"elapsed": ran, "actions": (OPEN,)}
|
|
223
|
+
if state is ItemState.FINISHED:
|
|
224
|
+
comp = session.comparisons.get(item.model_id)
|
|
225
|
+
# The test's verdict as it stands — the latest row's alone.
|
|
226
|
+
fields["status"] = (
|
|
227
|
+
None if not latest else classify(comp) if comp is not None else SIM_FAIL
|
|
228
|
+
)
|
|
229
|
+
fields["words"] = "finished" + (
|
|
230
|
+
f" · {ran:.0f} s" if ran is not None else ""
|
|
231
|
+
)
|
|
232
|
+
elif state is ItemState.CANCELLED:
|
|
233
|
+
fields.update(status=CANCELLED, words="cancelled")
|
|
234
|
+
else:
|
|
235
|
+
message = item.message or "the simulation failed"
|
|
236
|
+
fields.update(
|
|
237
|
+
status=SIM_FAIL if started else NOT_RUN,
|
|
238
|
+
words=f"failed: {one_line(message)}",
|
|
239
|
+
message=message,
|
|
240
|
+
)
|
|
241
|
+
return QueueRow(
|
|
242
|
+
number=number,
|
|
243
|
+
model_id=item.model_id,
|
|
244
|
+
state=state,
|
|
245
|
+
started=started,
|
|
246
|
+
latest=latest,
|
|
247
|
+
**fields,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def queue_rows(session: Session, *, now: float | None = None) -> list[QueueRow]:
|
|
252
|
+
"""The queue's rows, in run order — and after them a row per test a
|
|
253
|
+
click named whose read of the library is still out
|
|
254
|
+
(`Session.checking`): taken, waiting, with no item yet. ``now`` is the
|
|
255
|
+
clock a running row's elapsed seconds are read against
|
|
256
|
+
(``time.time()``)."""
|
|
257
|
+
clock = time.time() if now is None else now
|
|
258
|
+
starting = {w.worker_id: w for w in session.starting_workers}
|
|
259
|
+
rows: list[QueueRow] = []
|
|
260
|
+
ahead = 0
|
|
261
|
+
last = {item.model_id: item for item in session.queue}
|
|
262
|
+
for number, item in enumerate(session.queue, start=1):
|
|
263
|
+
latest = last[item.model_id] is item
|
|
264
|
+
row = _row(session, item, number, ahead, clock, starting, latest)
|
|
265
|
+
if row.state in (ItemState.WAITING, ItemState.RUNNING):
|
|
266
|
+
ahead += 1
|
|
267
|
+
rows.append(row)
|
|
268
|
+
for model_id in session.checking:
|
|
269
|
+
rows.append(
|
|
270
|
+
QueueRow(
|
|
271
|
+
number=len(rows) + 1,
|
|
272
|
+
model_id=model_id,
|
|
273
|
+
state=ItemState.WAITING,
|
|
274
|
+
words=f"waiting · {CHECKING}",
|
|
275
|
+
status=QUEUED,
|
|
276
|
+
ahead=ahead,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
ahead += 1
|
|
280
|
+
return rows
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def queue_row(
|
|
284
|
+
session: Session, model_id: str, *, now: float | None = None
|
|
285
|
+
) -> QueueRow | None:
|
|
286
|
+
"""The row of ``model_id``'s item — the last one listed — or None."""
|
|
287
|
+
return next(
|
|
288
|
+
(r for r in reversed(queue_rows(session, now=now)) if r.model_id == model_id),
|
|
289
|
+
None,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def counts_text(rows: list[QueueRow]) -> str:
|
|
294
|
+
"""The panel's header: how many rows are in each state, in the order a
|
|
295
|
+
queue passes through them; ``queue empty`` without a row."""
|
|
296
|
+
parts = [
|
|
297
|
+
f"{n} {word}"
|
|
298
|
+
for word, n in (
|
|
299
|
+
("running", sum(r.state is ItemState.RUNNING for r in rows)),
|
|
300
|
+
("queued", sum(r.state is ItemState.WAITING for r in rows)),
|
|
301
|
+
("finished", sum(r.state is ItemState.FINISHED for r in rows)),
|
|
302
|
+
("failed", sum(r.state is ItemState.FAILED for r in rows)),
|
|
303
|
+
("cancelled", sum(r.state is ItemState.CANCELLED for r in rows)),
|
|
304
|
+
)
|
|
305
|
+
if n
|
|
306
|
+
]
|
|
307
|
+
return " · ".join(parts) if parts else "queue empty"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass(frozen=True)
|
|
311
|
+
class Indicator:
|
|
312
|
+
"""The header's queue indicator: ``3 running · 5 queued``, ``idle``, or
|
|
313
|
+
— ``tone`` ``failed`` — the first words of the message the pool stopped
|
|
314
|
+
with, the whole of it in ``tip``."""
|
|
315
|
+
|
|
316
|
+
text: str
|
|
317
|
+
tone: str # "idle" | "running" | "failed"
|
|
318
|
+
tip: str
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def indicator(session: Session) -> Indicator:
|
|
322
|
+
rows = queue_rows(session)
|
|
323
|
+
running = sum(r.state is ItemState.RUNNING for r in rows)
|
|
324
|
+
queued = sum(r.state is ItemState.WAITING for r in rows)
|
|
325
|
+
busy = " · ".join(
|
|
326
|
+
f"{n} {word}" for word, n in (("running", running), ("queued", queued)) if n
|
|
327
|
+
)
|
|
328
|
+
stopped = session.queue_stopped
|
|
329
|
+
if stopped is not None:
|
|
330
|
+
line = one_line(stopped)
|
|
331
|
+
short = (
|
|
332
|
+
line
|
|
333
|
+
if len(line) <= INDICATOR_WIDTH
|
|
334
|
+
else line[:INDICATOR_WIDTH].rstrip() + "…"
|
|
335
|
+
)
|
|
336
|
+
return Indicator(
|
|
337
|
+
" · ".join(t for t in (short, busy) if t),
|
|
338
|
+
"failed",
|
|
339
|
+
f"{stopped} — Clear finished in the queue panel dismisses this.",
|
|
340
|
+
)
|
|
341
|
+
if busy:
|
|
342
|
+
return Indicator(busy, "running", "The simulation queue. Click to open it.")
|
|
343
|
+
return Indicator("idle", "idle", "Nothing is queued. Click to open the queue.")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def stop_reason(session: Session) -> str | None:
|
|
347
|
+
"""What Stop server says of the queue (brief §3.6: it confirms when the
|
|
348
|
+
queue is not empty): how many tests it still holds, or None."""
|
|
349
|
+
held = sum(
|
|
350
|
+
r.state in (ItemState.WAITING, ItemState.RUNNING) for r in queue_rows(session)
|
|
351
|
+
)
|
|
352
|
+
if not held:
|
|
353
|
+
return None
|
|
354
|
+
return f"{plural(held, 'test')} {'is' if held == 1 else 'are'} queued or running"
|