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
dstf/cli/review.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""The interactive review loop (`run -i`): accept / skip / detail / plot."""
|
|
2
|
+
|
|
3
|
+
from ..backends.types import label_with_name
|
|
4
|
+
from ..discovery.test_registry import TestModel
|
|
5
|
+
from ..reporting.status import (
|
|
6
|
+
FAIL,
|
|
7
|
+
NO_REF,
|
|
8
|
+
PASS,
|
|
9
|
+
SIM_FAIL,
|
|
10
|
+
STATUS_LABELS,
|
|
11
|
+
advisory_text,
|
|
12
|
+
classify,
|
|
13
|
+
failed_criteria,
|
|
14
|
+
nothing_ran,
|
|
15
|
+
refusal,
|
|
16
|
+
)
|
|
17
|
+
from ..storage.accept import accept_run_results
|
|
18
|
+
from .common import _exit_code, _parse_review_filter, _should_review
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _interactive_review(
|
|
22
|
+
tests: list[TestModel],
|
|
23
|
+
results: dict,
|
|
24
|
+
comparisons: list,
|
|
25
|
+
store,
|
|
26
|
+
config,
|
|
27
|
+
review_filter: str = "all",
|
|
28
|
+
browser: bool = True,
|
|
29
|
+
) -> int:
|
|
30
|
+
"""Interactively review each test and accept/reject results. A plot a
|
|
31
|
+
review asks for is opened in a browser tab when ``browser``."""
|
|
32
|
+
from ..backends import resolve_variable_patterns
|
|
33
|
+
|
|
34
|
+
# review 2026-07-06 (found while testing finding 62): the old import
|
|
35
|
+
# `from .backends.dymola.mat_reader import read_dymola_mat` pointed at
|
|
36
|
+
# a module removed in the backends/common refactor, so EVERY -i
|
|
37
|
+
# invocation crashed with ModuleNotFoundError before the loop started.
|
|
38
|
+
# The [v] path only needs variable names — use the names-only reader.
|
|
39
|
+
from ..backends.common.mat_reader import list_result_mat_variables
|
|
40
|
+
from ..discovery.spec_parser import update_test_variables
|
|
41
|
+
|
|
42
|
+
# Deferred: run_cmds imports this module at module scope, so the
|
|
43
|
+
# dependency back on its backend selection has to resolve at call time.
|
|
44
|
+
from .run_cmds import _get_backend
|
|
45
|
+
|
|
46
|
+
filters = _parse_review_filter(review_filter)
|
|
47
|
+
|
|
48
|
+
test_lookup = {t.model_id: t for t in tests}
|
|
49
|
+
n_accepted = 0
|
|
50
|
+
n_skipped = 0
|
|
51
|
+
n_auto_skipped = 0
|
|
52
|
+
# review 2026-07-06 finding 62: one consistent exit rule — after the
|
|
53
|
+
# review loop ends (completed OR quit), exit 1 iff any test the loop
|
|
54
|
+
# passed over (reviewed, user-skipped, or auto-skipped) is still
|
|
55
|
+
# failing/sim-failed and was not accepted during this session; else 0.
|
|
56
|
+
# Tests never reached before a quit were neither reviewed nor skipped,
|
|
57
|
+
# so they don't count. A review in which nothing ran exits 1, as `run`
|
|
58
|
+
# does (D145 §5); it can only complete, since only a test that ran is
|
|
59
|
+
# offered a prompt to quit at.
|
|
60
|
+
n_unresolved = 0
|
|
61
|
+
|
|
62
|
+
def _still_failing(c) -> bool:
|
|
63
|
+
# A test NOT RUN has no result: it fails nothing (D145).
|
|
64
|
+
return c.ran and ((not c.sim_success) or not c.passed)
|
|
65
|
+
|
|
66
|
+
n_total = len(comparisons)
|
|
67
|
+
|
|
68
|
+
spec_path = config.spec_path
|
|
69
|
+
|
|
70
|
+
# ANSI colors
|
|
71
|
+
GREEN = "\033[92m"
|
|
72
|
+
RED = "\033[91m"
|
|
73
|
+
YELLOW = "\033[93m"
|
|
74
|
+
BOLD = "\033[1m"
|
|
75
|
+
RESET = "\033[0m"
|
|
76
|
+
status_colors = {PASS: GREEN, FAIL: RED, SIM_FAIL: RED, NO_REF: YELLOW}
|
|
77
|
+
|
|
78
|
+
filter_desc = f" (filter: {review_filter})" if "all" not in filters else ""
|
|
79
|
+
print(f"\nInteractive review: {n_total} tests{filter_desc}\n")
|
|
80
|
+
|
|
81
|
+
for idx, comp in enumerate(comparisons, 1):
|
|
82
|
+
if not _should_review(comp, filters):
|
|
83
|
+
n_auto_skipped += 1
|
|
84
|
+
if _still_failing(comp):
|
|
85
|
+
n_unresolved += 1
|
|
86
|
+
continue
|
|
87
|
+
model_id = comp.model_id
|
|
88
|
+
|
|
89
|
+
# Status line: the one classifier's status (`reporting.status`), as
|
|
90
|
+
# every other surface shows it — keyed on `has_reference` instead, a
|
|
91
|
+
# refused test with no baseline read "NO BASELINE" above its error.
|
|
92
|
+
state = classify(comp)
|
|
93
|
+
status = "NO BASELINE" if state == NO_REF else STATUS_LABELS[state]
|
|
94
|
+
if state in status_colors:
|
|
95
|
+
status = f"{status_colors[state]}{status}{RESET}"
|
|
96
|
+
|
|
97
|
+
warn = ""
|
|
98
|
+
if comp.warnings:
|
|
99
|
+
warn = f" {YELLOW}[{len(comp.warnings)} warning(s)]{RESET}"
|
|
100
|
+
|
|
101
|
+
if comp.test_id:
|
|
102
|
+
id_tag = f"[{comp.test_id}] "
|
|
103
|
+
elif not comp.has_reference:
|
|
104
|
+
id_tag = "[new] "
|
|
105
|
+
else:
|
|
106
|
+
id_tag = ""
|
|
107
|
+
print(f"[{idx}/{n_total}] {BOLD}{id_tag}{model_id}{RESET}")
|
|
108
|
+
print(f" Status: {status}{warn}")
|
|
109
|
+
|
|
110
|
+
# A refused test's reason is its whole finding: there is no failing
|
|
111
|
+
# variable below to say it, and accepting it is refused.
|
|
112
|
+
refused = refusal(comp)
|
|
113
|
+
if comp.error_message and (not comp.sim_success or refused):
|
|
114
|
+
print(f" Error: {comp.error_message}")
|
|
115
|
+
|
|
116
|
+
# Its criteria in brief — `comp.variables` holds one comparison per
|
|
117
|
+
# CRITERION (two on one variable are two): the Criteria column's
|
|
118
|
+
# count, under its name.
|
|
119
|
+
if comp.variables:
|
|
120
|
+
n_pass = sum(1 for v in comp.variables if v.passed)
|
|
121
|
+
# Its own name: as `n_total` it replaced the number of TESTS,
|
|
122
|
+
# and every later header read `[2/<the last test's criteria>]`.
|
|
123
|
+
n_criteria = len(comp.variables)
|
|
124
|
+
# What did not pass is read where the console's blocks and
|
|
125
|
+
# JUnit's lines read it: a failure under `warn` fails no test,
|
|
126
|
+
# and counted with the ones that do, this line said `2 failed`
|
|
127
|
+
# of a test JUnit called `1 criterion failed, with 1 advisory
|
|
128
|
+
# failure`.
|
|
129
|
+
not_passed = failed_criteria(comp)
|
|
130
|
+
n_advisory = sum(1 for c in not_passed if c.advisory)
|
|
131
|
+
said = [f"{n_pass}/{n_criteria} passed"]
|
|
132
|
+
if not comp.passed:
|
|
133
|
+
said.append(f"{len(not_passed) - n_advisory} failed")
|
|
134
|
+
if n_advisory:
|
|
135
|
+
said.append(advisory_text(n_advisory))
|
|
136
|
+
print(f" Criteria: {', '.join(said)}")
|
|
137
|
+
|
|
138
|
+
# Can't accept if simulation failed, or never ran
|
|
139
|
+
if not comp.sim_success:
|
|
140
|
+
print(f" Skipping ({'simulation failed' if comp.ran else 'not run'})\n")
|
|
141
|
+
n_skipped += 1
|
|
142
|
+
n_unresolved += _still_failing(comp)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Prompt
|
|
146
|
+
result = results.get(model_id)
|
|
147
|
+
has_result = result is not None and result.success
|
|
148
|
+
can_accept = has_result and not refused
|
|
149
|
+
test = test_lookup.get(model_id)
|
|
150
|
+
added_patterns = [] # Track patterns added via [v] this session
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
if can_accept:
|
|
154
|
+
prompt = " [a]ccept [v]ariables [s]kip [d]etail [p]lot [q]uit > "
|
|
155
|
+
elif has_result:
|
|
156
|
+
prompt = " [v]ariables [s]kip [d]etail [p]lot [q]uit > "
|
|
157
|
+
else:
|
|
158
|
+
prompt = " [s]kip [d]etail [p]lot [q]uit > "
|
|
159
|
+
try:
|
|
160
|
+
choice = input(prompt).strip().lower()
|
|
161
|
+
except (EOFError, KeyboardInterrupt):
|
|
162
|
+
print("\nAborted.")
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
if choice == "a" and can_accept:
|
|
166
|
+
# If variables were added, re-read the .mat to resolve them
|
|
167
|
+
if added_patterns:
|
|
168
|
+
test.variable_patterns = list(
|
|
169
|
+
set(test.variable_patterns + added_patterns)
|
|
170
|
+
)
|
|
171
|
+
test_key = _find_test_key(model_id, config)
|
|
172
|
+
if test_key:
|
|
173
|
+
backend = _get_backend(config)
|
|
174
|
+
# The run's statistics and timing come back
|
|
175
|
+
# from the file it kept beside the result.
|
|
176
|
+
result = backend.read_run_result(test, test_key, None)
|
|
177
|
+
|
|
178
|
+
# The one accept path: `accept_run_results` refuses here
|
|
179
|
+
# exactly what `compare --accept` refuses — a poisoned
|
|
180
|
+
# trajectory (leftover B: it must not crash the loop) and a
|
|
181
|
+
# test the last run left pending — and stores no baseline for
|
|
182
|
+
# a simulate-only test, which it names in a note: neither
|
|
183
|
+
# stored nor refused.
|
|
184
|
+
# `test` is never missing on the `[a]` path: `cmd_run` hands the
|
|
185
|
+
# same `scope_tests` to `compare_all` (one comparison per test)
|
|
186
|
+
# and to this function, and `RunOutcome.results` is keyed by
|
|
187
|
+
# exactly those models — so a model with a comparison and a
|
|
188
|
+
# result has an entry in `test_lookup`.
|
|
189
|
+
stored, skipped, warnings, notes = accept_run_results(
|
|
190
|
+
store, [test], {test.model_id: result}, config.work_dir
|
|
191
|
+
)
|
|
192
|
+
if stored:
|
|
193
|
+
n_vars = len(result.variables) if result.variables else 0
|
|
194
|
+
print(f" {GREEN}Accepted ({n_vars} variables).{RESET}")
|
|
195
|
+
# A large baseline file: stored, and said.
|
|
196
|
+
for line in warnings:
|
|
197
|
+
print(f" {YELLOW}{line}{RESET}")
|
|
198
|
+
print()
|
|
199
|
+
n_accepted += 1
|
|
200
|
+
else:
|
|
201
|
+
if skipped:
|
|
202
|
+
print(f" {RED}Not accepted: {skipped[0][1]}{RESET}\n")
|
|
203
|
+
else:
|
|
204
|
+
# A simulate-only test: nothing was refused, and
|
|
205
|
+
# there is no baseline to store — the accept's own
|
|
206
|
+
# line, as `--accept` prints it.
|
|
207
|
+
for line in notes:
|
|
208
|
+
print(f" {line}")
|
|
209
|
+
print()
|
|
210
|
+
n_skipped += 1
|
|
211
|
+
if _still_failing(comp):
|
|
212
|
+
n_unresolved += 1
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
elif choice == "v" and has_result:
|
|
216
|
+
try:
|
|
217
|
+
raw = input(" Variable patterns (comma-separated): ").strip()
|
|
218
|
+
except (EOFError, KeyboardInterrupt):
|
|
219
|
+
print("\nAborted.")
|
|
220
|
+
return 1
|
|
221
|
+
if raw:
|
|
222
|
+
new_patterns = [p.strip() for p in raw.split(",") if p.strip()]
|
|
223
|
+
added_patterns.extend(new_patterns)
|
|
224
|
+
# Save to test_spec.json
|
|
225
|
+
if spec_path:
|
|
226
|
+
update_test_variables(spec_path, model_id, new_patterns)
|
|
227
|
+
print(
|
|
228
|
+
f" Added {len(new_patterns)} pattern(s) to {spec_path.name}"
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
print(
|
|
232
|
+
f" Added {len(new_patterns)} pattern(s) (in-memory only, no spec file configured)"
|
|
233
|
+
)
|
|
234
|
+
# Show what would match from the .mat
|
|
235
|
+
test_key = _find_test_key(model_id, config)
|
|
236
|
+
if test_key:
|
|
237
|
+
test_dir = config.work_dir / test_key
|
|
238
|
+
mat_path = test_dir / "dsres.mat"
|
|
239
|
+
if mat_path.exists():
|
|
240
|
+
available = list_result_mat_variables(mat_path)
|
|
241
|
+
if available:
|
|
242
|
+
matched = resolve_variable_patterns(
|
|
243
|
+
new_patterns, available
|
|
244
|
+
)
|
|
245
|
+
print(
|
|
246
|
+
f" Matched {len(matched)} variables: {', '.join(matched[:10])}"
|
|
247
|
+
)
|
|
248
|
+
if len(matched) > 10:
|
|
249
|
+
print(f" ... and {len(matched) - 10} more")
|
|
250
|
+
|
|
251
|
+
elif choice == "s":
|
|
252
|
+
print(" Skipped.\n")
|
|
253
|
+
n_skipped += 1
|
|
254
|
+
if _still_failing(comp):
|
|
255
|
+
n_unresolved += 1
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
elif choice == "d":
|
|
259
|
+
_print_detail(comp)
|
|
260
|
+
|
|
261
|
+
elif choice == "p":
|
|
262
|
+
_generate_and_open_plots(
|
|
263
|
+
model_id, comp, result, store, config, test, browser=browser
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
elif choice == "q":
|
|
267
|
+
auto = f", {n_auto_skipped} auto-skipped" if n_auto_skipped else ""
|
|
268
|
+
print(f"\nStopped. Accepted {n_accepted}, skipped {n_skipped}{auto}.")
|
|
269
|
+
# review 2026-07-06 finding 62: quit uses the same rule as
|
|
270
|
+
# completion — unresolved failures seen so far decide.
|
|
271
|
+
return 1 if n_unresolved else 0
|
|
272
|
+
|
|
273
|
+
else:
|
|
274
|
+
options = (
|
|
275
|
+
"a/v/s/d/p/q"
|
|
276
|
+
if can_accept
|
|
277
|
+
else "v/s/d/p/q"
|
|
278
|
+
if has_result
|
|
279
|
+
else "s/d/p/q"
|
|
280
|
+
)
|
|
281
|
+
print(f" Invalid choice. Enter {options}.")
|
|
282
|
+
|
|
283
|
+
auto = f", {n_auto_skipped} auto-skipped" if n_auto_skipped else ""
|
|
284
|
+
print(f"\nDone. Accepted {n_accepted}, skipped {n_skipped}{auto}.")
|
|
285
|
+
# review 2026-07-06 finding 62: see n_unresolved above. A review in
|
|
286
|
+
# which nothing ran exits as `run` does, saying so (D145 §5).
|
|
287
|
+
if nothing_ran(comparisons):
|
|
288
|
+
return _exit_code(comparisons)
|
|
289
|
+
return 1 if n_unresolved else 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _generate_and_open_plots(
|
|
293
|
+
model_id, comp, result, store, config, test=None, *, browser: bool = True
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Generate comparison plots, print their link and — when ``browser`` —
|
|
296
|
+
open them in a browser tab."""
|
|
297
|
+
from ..reporting.report_suite import (
|
|
298
|
+
generate_comparison_plots,
|
|
299
|
+
open_in_browser,
|
|
300
|
+
plot_inputs,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
test_key = _find_test_key(model_id, config)
|
|
304
|
+
if test_key:
|
|
305
|
+
plot_dir = config.work_dir / test_key / "plots"
|
|
306
|
+
else:
|
|
307
|
+
plot_dir = config.work_dir / "plots" / model_id.replace(".", "_")
|
|
308
|
+
|
|
309
|
+
ref_data = store.get_reference(model_id)
|
|
310
|
+
|
|
311
|
+
test_dir = config.work_dir / test_key if test_key else None
|
|
312
|
+
|
|
313
|
+
# Resolve reference file path for clickable link in report
|
|
314
|
+
ref_file = None
|
|
315
|
+
test_id = store.index.get_id(model_id)
|
|
316
|
+
if test_id:
|
|
317
|
+
from ..storage.ref_index import RefIndex
|
|
318
|
+
|
|
319
|
+
ref_file = store.ref_dir / RefIndex.ref_filename(test_id)
|
|
320
|
+
|
|
321
|
+
from ..backends import get_backend_class
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
backend_cls = get_backend_class(config)
|
|
325
|
+
artifact_files = tuple(backend_cls.artifact_files)
|
|
326
|
+
except ValueError:
|
|
327
|
+
backend_cls = None
|
|
328
|
+
artifact_files = ()
|
|
329
|
+
|
|
330
|
+
# The page's layout, through the one helper the suite path uses.
|
|
331
|
+
plots, extra_series = plot_inputs(
|
|
332
|
+
test, comp.variables, result, backend_cls, test_dir
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
html_path = generate_comparison_plots(
|
|
336
|
+
model_id=model_id,
|
|
337
|
+
ref_data=ref_data,
|
|
338
|
+
result=result,
|
|
339
|
+
comparisons=comp.variables,
|
|
340
|
+
plot_dir=plot_dir,
|
|
341
|
+
test_dir=test_dir,
|
|
342
|
+
test_model=test,
|
|
343
|
+
ref_file=ref_file,
|
|
344
|
+
warnings=comp.warnings,
|
|
345
|
+
comparison=comp,
|
|
346
|
+
metric_tree=comp.metric_tree,
|
|
347
|
+
artifact_files=artifact_files,
|
|
348
|
+
max_embedded_samples=config.max_embedded_samples,
|
|
349
|
+
theme=config.theme,
|
|
350
|
+
plots=plots,
|
|
351
|
+
extra_series=extra_series,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if html_path:
|
|
355
|
+
print(f" Plots saved to {plot_dir}")
|
|
356
|
+
open_in_browser(html_path, launch=browser)
|
|
357
|
+
else:
|
|
358
|
+
print(" Plot generation failed")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _find_test_key(model_id: str, config) -> str | None:
|
|
362
|
+
"""Find the test_NNNN key for a model from the batch manifest."""
|
|
363
|
+
from ..backends import BatchManifest
|
|
364
|
+
|
|
365
|
+
mp = config.work_dir / "batch_manifest.json"
|
|
366
|
+
if not mp.exists():
|
|
367
|
+
return None
|
|
368
|
+
bm = BatchManifest.load(mp)
|
|
369
|
+
for tk, entry in bm.manifest.items():
|
|
370
|
+
if entry["model_id"] == model_id:
|
|
371
|
+
return tk
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _print_detail(comp) -> None:
|
|
376
|
+
"""Print each leaf's mode-aware verdict for interactive review."""
|
|
377
|
+
from ..comparison.registry import summarize
|
|
378
|
+
|
|
379
|
+
if comp.error_message:
|
|
380
|
+
print(f" Error: {comp.error_message}")
|
|
381
|
+
|
|
382
|
+
for var in comp.variables:
|
|
383
|
+
name = label_with_name(var.name, var.expression)
|
|
384
|
+
status = "\033[92mPASS\033[0m" if var.passed else "\033[91mFAIL\033[0m"
|
|
385
|
+
summary = summarize(var)
|
|
386
|
+
# The flat-route unknown-mode sentinel carries mode="" on purpose
|
|
387
|
+
# (it must not be captioned "nrmse"); print no bracket for it.
|
|
388
|
+
mode = f"[{var.mode}] " if var.mode else ""
|
|
389
|
+
print(f" {status} {name} {mode}{summary.display}")
|
|
390
|
+
print(f" {summary.criterion}")
|
|
391
|
+
for key, value in summary.details:
|
|
392
|
+
print(f" {key}: {value}")
|
|
393
|
+
|
|
394
|
+
if comp.warnings:
|
|
395
|
+
print(" Structural warnings:")
|
|
396
|
+
for w in comp.warnings:
|
|
397
|
+
print(f" {w.text}")
|
|
398
|
+
print()
|