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,277 @@
|
|
|
1
|
+
"""The Suite page (brief §3.2 "two pages", §3.8; DESIGN.md "Tables"): what a
|
|
2
|
+
batch acts on. The header, the progress line, one toolbar — the count pills
|
|
3
|
+
and the view toggles (drawn by the table's script), the selection's label,
|
|
4
|
+
the batch actions as icons — the Suite table (`suite_grid.py`) and, beside
|
|
5
|
+
it, the queue panel (`queue_panel.py`). The table filters, sorts and groups in the browser; Python sends its
|
|
6
|
+
rows and receives its selection, which lives in the tab (`tabstate.py`),
|
|
7
|
+
never in the Session (D129). One object per browser tab."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from nicegui import ui
|
|
15
|
+
|
|
16
|
+
from ...reporting.rows import TestRow
|
|
17
|
+
from ..capabilities import Placement
|
|
18
|
+
from ..session import Selection
|
|
19
|
+
from ..suite_rows import suite_rows
|
|
20
|
+
from .actions import ActionBar
|
|
21
|
+
from .header import Header
|
|
22
|
+
from .queue_panel import ANCHOR, QueuePanel
|
|
23
|
+
from .suite_grid import GRID_CSS, STORAGE, SuiteGrid, scripts_html
|
|
24
|
+
from .tab_theme import first_paint
|
|
25
|
+
from .tabstate import TabState, caught_up, connected_tab_state, heard_spec
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .shell import Shell
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SuitePage:
|
|
34
|
+
def __init__(self, shell: Shell) -> None:
|
|
35
|
+
self.shell = shell
|
|
36
|
+
self.client: Any = None
|
|
37
|
+
#: The tests the grid shows once this tick is flushed: the visible
|
|
38
|
+
#: tests, in the project's order.
|
|
39
|
+
self.ids: list[str] = []
|
|
40
|
+
#: The tests the grid holds now: what the last flush sent.
|
|
41
|
+
self._sent: list[str] = []
|
|
42
|
+
#: What this tick's events touched, repainted by `flush` in one
|
|
43
|
+
#: transaction.
|
|
44
|
+
self._update: set[str] = set()
|
|
45
|
+
#: The last flush raised building its rows: this one builds them one
|
|
46
|
+
#: by one and leaves out the ones that raise.
|
|
47
|
+
self._retrying = False
|
|
48
|
+
#: Visible tests whose row failed to build when they were to be
|
|
49
|
+
#: added: not in the grid, and added again once an event touches
|
|
50
|
+
#: them — or by the one retry below.
|
|
51
|
+
self._dropped: set[str] = set()
|
|
52
|
+
#: A retrying flush dropped rows: the next flush that builds tries
|
|
53
|
+
#: them once more, one by one. The failure may have been no one
|
|
54
|
+
#: row's (every build raising for two ticks dropped every row).
|
|
55
|
+
self._redo = False
|
|
56
|
+
|
|
57
|
+
async def build(self) -> None:
|
|
58
|
+
self.client = ui.context.client
|
|
59
|
+
# BEFORE the handshake, so they are in the page's first HTML: after
|
|
60
|
+
# it, `add_head_html` inserts through insertAdjacentHTML, which
|
|
61
|
+
# never runs a <script> — the grid drew no rows, "DstfSuite is not
|
|
62
|
+
# defined".
|
|
63
|
+
ui.add_head_html(scripts_html(f"{STORAGE}:{self._project_key()}"))
|
|
64
|
+
# The grid's height too: added after the handshake, it arrived after
|
|
65
|
+
# the grid was built whenever the scripts came from the browser's
|
|
66
|
+
# cache — every second load — and AG Grid, ready at NiceGUI's
|
|
67
|
+
# default 256 px, drew the first 15 rows of a 710 px table and left
|
|
68
|
+
# the rest blank until a scroll or a resize.
|
|
69
|
+
ui.add_css(GRID_CSS)
|
|
70
|
+
first_paint(self.shell)
|
|
71
|
+
state = await connected_tab_state(self.client)
|
|
72
|
+
if state is None:
|
|
73
|
+
return
|
|
74
|
+
self.state: TabState = state
|
|
75
|
+
caught_up(state, self.shell.session.spec_revision)
|
|
76
|
+
# The grid takes a switch by itself: its colours are the tokens, and
|
|
77
|
+
# NiceGUI turns its scheme with `body--dark`.
|
|
78
|
+
self.theme = self.shell.page_setup(state)
|
|
79
|
+
ui.page_title("DSTF")
|
|
80
|
+
self.header = Header(self.shell, self.theme)
|
|
81
|
+
with ui.column().classes("dstf-suite w-full q-pa-md gap-3"):
|
|
82
|
+
self.progress = ui.label().classes("dstf-progress").mark("progress")
|
|
83
|
+
with ui.column().classes("gap-1") as self.hint:
|
|
84
|
+
ui.label("No project is open. Start the tool on one:")
|
|
85
|
+
ui.code("dstf author --config <path/to/testing.json>")
|
|
86
|
+
with ui.row().classes(
|
|
87
|
+
"dstf-suite-toolbar items-center w-full gap-3 no-wrap"
|
|
88
|
+
):
|
|
89
|
+
# Both drawn by `suite_grid.js`; Python never updates them.
|
|
90
|
+
ui.element("div").props("id=dstf-suite-tools").mark("tools")
|
|
91
|
+
ui.space()
|
|
92
|
+
ui.element("div").props("id=dstf-suite-selection").mark("selected")
|
|
93
|
+
self.actions = ActionBar(
|
|
94
|
+
self.shell,
|
|
95
|
+
Placement.SUITE,
|
|
96
|
+
lambda: Selection(tests=frozenset(self.state.checked)),
|
|
97
|
+
marker="suite-actions",
|
|
98
|
+
icons=True,
|
|
99
|
+
stale=self.header.stale,
|
|
100
|
+
)
|
|
101
|
+
self.ids = self.shell.visible_tests()
|
|
102
|
+
self._sent = list(self.ids)
|
|
103
|
+
with ui.element("div").classes("dstf-suite-body"):
|
|
104
|
+
with ui.element("div").classes("dstf-suite-main"):
|
|
105
|
+
self.grid = SuiteGrid(
|
|
106
|
+
self._rows(self.ids),
|
|
107
|
+
selected=self.state.checked,
|
|
108
|
+
on_selection=self._on_selection,
|
|
109
|
+
).mark("grid")
|
|
110
|
+
self.queue = QueuePanel(self.shell, stale=self.header.stale)
|
|
111
|
+
# The header's indicator links here from every page; the panel is
|
|
112
|
+
# drawn after the browser looked for the anchor.
|
|
113
|
+
ui.run_javascript(
|
|
114
|
+
f"if (location.hash === '#{ANCHOR}') "
|
|
115
|
+
f"document.getElementById('{ANCHOR}')?.scrollIntoView();"
|
|
116
|
+
)
|
|
117
|
+
self.grid.seal()
|
|
118
|
+
self.shell.register(self)
|
|
119
|
+
self.refresh()
|
|
120
|
+
|
|
121
|
+
def _project_key(self) -> str:
|
|
122
|
+
"""What names this project for the table's stored view: its config
|
|
123
|
+
file, resolved — known before the open and the same after it (the
|
|
124
|
+
work dir is derived by the open when `--work-dir` is not given).
|
|
125
|
+
`dstf author` refuses to start without a config, so there always is
|
|
126
|
+
one. A reopen on another backend keeps the view."""
|
|
127
|
+
return str(self.shell.config_path.resolve())
|
|
128
|
+
|
|
129
|
+
def _rows(self, ids: list[str]) -> list[TestRow]:
|
|
130
|
+
if self.shell.known:
|
|
131
|
+
# An open is under way: the Session's comparisons are the
|
|
132
|
+
# previous project's, or none. Its names wait for its verdicts.
|
|
133
|
+
return [TestRow(model_id=m, waiting=True) for m in ids]
|
|
134
|
+
return suite_rows(
|
|
135
|
+
self.shell.session, ids, failed=set(self.shell.load.failed_models)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# -- the selection ---------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def _on_selection(self, ids: list[str]) -> None:
|
|
141
|
+
"""The grid's selection, from the browser: the tab's (D129)."""
|
|
142
|
+
self.state.checked = set(ids)
|
|
143
|
+
self.actions.refresh()
|
|
144
|
+
|
|
145
|
+
def refresh(self) -> None:
|
|
146
|
+
load = self.shell.load
|
|
147
|
+
text = load.text()
|
|
148
|
+
self.progress.set_text(text)
|
|
149
|
+
self.progress.set_visibility(bool(text))
|
|
150
|
+
self.hint.set_visibility(
|
|
151
|
+
self.shell.session.project is None and not load.in_progress
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# -- the Session's events --------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def handle(self, event: dict[str, Any]) -> None:
|
|
157
|
+
"""Every Session event, from the shell's tick. No early return: the
|
|
158
|
+
panel, the actions and the rows are independent regions, and a
|
|
159
|
+
`spec` (a close, an `OpenProject`) is exactly the event after which
|
|
160
|
+
the panel must not show the previous project's queue. The rows an
|
|
161
|
+
event touches are collected; `flush` sends them."""
|
|
162
|
+
self.header.handle(event)
|
|
163
|
+
kind = event["event"]
|
|
164
|
+
if kind == "spec":
|
|
165
|
+
# The tab's folds: every page of it hears the rule (K1).
|
|
166
|
+
heard_spec(self.state, event)
|
|
167
|
+
self.queue.handle(event)
|
|
168
|
+
if kind in ("results", "queue", "notice", "spec", "opened") or (
|
|
169
|
+
kind == "loading" and event.get("phase") == "done"
|
|
170
|
+
):
|
|
171
|
+
self.actions.refresh()
|
|
172
|
+
if kind in ("spec", "known"):
|
|
173
|
+
# A `spec` event names only the CHANGED models (an open's names
|
|
174
|
+
# all, before the CLI's filter; a close's none), and `known` the
|
|
175
|
+
# models an open named before its scan: what to add and drop is
|
|
176
|
+
# the visible tests against the grid's, at the flush.
|
|
177
|
+
self.ids = self.shell.visible_tests()
|
|
178
|
+
self._update |= self._touched(event) & set(self.ids)
|
|
179
|
+
self.refresh()
|
|
180
|
+
|
|
181
|
+
def _touched(self, event: dict[str, Any]) -> set[str]:
|
|
182
|
+
"""The models whose row an event may have changed."""
|
|
183
|
+
kind = event["event"]
|
|
184
|
+
if kind == "known":
|
|
185
|
+
# Every row: an open's names wait, and its failure's `known`
|
|
186
|
+
# (naming none) turns every row back to the open project's
|
|
187
|
+
# verdict — repainting only the event's models left every row
|
|
188
|
+
# reading "…" after a failed reopen.
|
|
189
|
+
return set(self.ids)
|
|
190
|
+
if kind in ("spec", "results", "queue"):
|
|
191
|
+
return {str(m) for m in event.get("models", ())}
|
|
192
|
+
if kind == "loading" and event.get("phase") == "results":
|
|
193
|
+
return {str(event["model"])}
|
|
194
|
+
return set()
|
|
195
|
+
|
|
196
|
+
def flush(self) -> None:
|
|
197
|
+
"""This tick's rows, as ONE transaction: the Accept path queues N + 2
|
|
198
|
+
events in a tick, and one transaction per event was 333 over 331
|
|
199
|
+
rows. The rows to add and drop are the visible tests against the
|
|
200
|
+
grid's — a model named and dropped within the tick is sent neither,
|
|
201
|
+
so the grid is never told to drop a row it never had, and one
|
|
202
|
+
dropped and named again stays (repainted if an event touched it).
|
|
203
|
+
An added row is sent once, with the tab's selection. What the grid
|
|
204
|
+
holds moves only once the transaction is sent: one that raises is
|
|
205
|
+
sent again on the next tick.
|
|
206
|
+
|
|
207
|
+
Retried once: when building the rows raised, the next tick builds
|
|
208
|
+
them one by one, sends the ones that build and moves on — one row
|
|
209
|
+
that always raises froze the grid (a traceback per tick, no update
|
|
210
|
+
sent). A row left out keeps its last painted state; one that was to
|
|
211
|
+
be added is not in the grid, and is added again once an event
|
|
212
|
+
touches it, or by the next flush that builds: that one tries every
|
|
213
|
+
dropped row once more, one by one, and leaves out without a log
|
|
214
|
+
line the ones that still raise — a failure of every row for two
|
|
215
|
+
ticks dropped them all, and none came back until touched."""
|
|
216
|
+
held, sent = set(self.ids), set(self._sent)
|
|
217
|
+
self._dropped &= held
|
|
218
|
+
add = [
|
|
219
|
+
m
|
|
220
|
+
for m in self.ids
|
|
221
|
+
if m not in sent and (m not in self._dropped or m in self._update)
|
|
222
|
+
]
|
|
223
|
+
remove = [m for m in self._sent if m not in held]
|
|
224
|
+
update = [m for m in self.ids if m in sent and m in self._update]
|
|
225
|
+
retrying = self._retrying
|
|
226
|
+
redo = (
|
|
227
|
+
[m for m in self.ids if m in self._dropped and m not in add]
|
|
228
|
+
if self._redo and not retrying
|
|
229
|
+
else []
|
|
230
|
+
)
|
|
231
|
+
added = set(add)
|
|
232
|
+
if add or update or remove or redo:
|
|
233
|
+
if retrying:
|
|
234
|
+
add_rows, bad_add = self._rows_each(add)
|
|
235
|
+
update_rows, bad_update = self._rows_each(update)
|
|
236
|
+
if bad_add or bad_update:
|
|
237
|
+
logger.error(
|
|
238
|
+
"the Suite table left out %s: %s",
|
|
239
|
+
", ".join(bad_add + bad_update),
|
|
240
|
+
"its row failed to build again",
|
|
241
|
+
)
|
|
242
|
+
added -= set(bad_add)
|
|
243
|
+
self._redo = self._redo or bool(bad_add)
|
|
244
|
+
else:
|
|
245
|
+
try:
|
|
246
|
+
add_rows, update_rows = self._rows(add), self._rows(update)
|
|
247
|
+
except Exception:
|
|
248
|
+
self._retrying = True
|
|
249
|
+
raise # the shell logs it; the next tick goes row by row
|
|
250
|
+
redo_rows, bad_redo = self._rows_each(redo)
|
|
251
|
+
add_rows += redo_rows
|
|
252
|
+
added |= set(redo) - set(bad_redo)
|
|
253
|
+
self.grid.transaction(
|
|
254
|
+
add=add_rows,
|
|
255
|
+
update=update_rows,
|
|
256
|
+
remove=remove,
|
|
257
|
+
select=[
|
|
258
|
+
m for m in add + redo if m in added and m in self.state.checked
|
|
259
|
+
],
|
|
260
|
+
)
|
|
261
|
+
self._dropped = (self._dropped - added) | (set(add) - added)
|
|
262
|
+
if not retrying:
|
|
263
|
+
self._redo = False # tried, or nothing left to try
|
|
264
|
+
self._retrying = False
|
|
265
|
+
self._sent = [m for m in self.ids if m in sent or m in added]
|
|
266
|
+
self._update = set()
|
|
267
|
+
|
|
268
|
+
def _rows_each(self, ids: list[str]) -> tuple[list[TestRow], list[str]]:
|
|
269
|
+
"""``ids``' rows built one at a time, and the ids whose build raised."""
|
|
270
|
+
rows: list[TestRow] = []
|
|
271
|
+
bad: list[str] = []
|
|
272
|
+
for m in ids:
|
|
273
|
+
try:
|
|
274
|
+
rows += self._rows([m])
|
|
275
|
+
except Exception:
|
|
276
|
+
bad.append(m)
|
|
277
|
+
return rows, bad
|