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/main.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""Argument-parser construction, the subcommand dispatch table, and the
|
|
2
|
+
process entry points."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ..config import ConfigError
|
|
8
|
+
from .author_cmd import cmd_author
|
|
9
|
+
from .baseline_cmds import cmd_companion, cmd_import_baseline, cmd_soft_check
|
|
10
|
+
from .common import _parse_review_filter
|
|
11
|
+
from .diag_cmds import (
|
|
12
|
+
cmd_check_dymola,
|
|
13
|
+
cmd_check_julia,
|
|
14
|
+
cmd_check_openmodelica,
|
|
15
|
+
cmd_discover,
|
|
16
|
+
cmd_export,
|
|
17
|
+
cmd_manifest,
|
|
18
|
+
)
|
|
19
|
+
from .run_cmds import cmd_compare, cmd_run
|
|
20
|
+
from .spec_cmds import cmd_add, cmd_export_schema, cmd_spec_update
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _review_filter_arg(value: str) -> str:
|
|
24
|
+
"""argparse ``type=`` callback for ``-i`` / ``--rerun`` category lists.
|
|
25
|
+
|
|
26
|
+
review 2026-07-06 finding 58: validate at parse time so a typo like
|
|
27
|
+
``-i sim_failed`` aborts in under a second with the valid-categories
|
|
28
|
+
message (argparse exit code 2) — not after the full simulation run.
|
|
29
|
+
"""
|
|
30
|
+
_parse_review_filter(value) # raises argparse.ArgumentTypeError on typos
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _add_global_flags(parser: argparse.ArgumentParser, *, subcommand: bool) -> None:
|
|
35
|
+
"""Attach the config-locating flags every subcommand needs.
|
|
36
|
+
|
|
37
|
+
Called twice for each flag: once on the top-level parser (so
|
|
38
|
+
``dstf --config X run`` works) and once on a parent parser that every
|
|
39
|
+
subparser inherits (so ``dstf run --config X`` works too). Both
|
|
40
|
+
positions must work — the flag-before-subcommand-only form was the
|
|
41
|
+
single biggest first-contact stumble.
|
|
42
|
+
|
|
43
|
+
*subcommand* switches the default to :data:`argparse.SUPPRESS`, and that
|
|
44
|
+
is load-bearing: ``_SubParsersAction`` parses the subcommand's arguments
|
|
45
|
+
into a fresh namespace and then copies **every** attribute of it onto the
|
|
46
|
+
top-level namespace. A plain ``default=None`` on the subparser copy would
|
|
47
|
+
therefore erase a value given before the subcommand. SUPPRESS leaves the
|
|
48
|
+
attribute unset when the flag isn't passed there, so the top-level value
|
|
49
|
+
survives; the top-level copy (``subcommand=False``) is what guarantees
|
|
50
|
+
the attribute exists at all.
|
|
51
|
+
"""
|
|
52
|
+
default = argparse.SUPPRESS if subcommand else None
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--source-path",
|
|
55
|
+
type=str,
|
|
56
|
+
default=default,
|
|
57
|
+
help="Path to the source for the library under test (Modelica package dir containing package.mo, FMU dir, ...). Default: auto-detect from cwd.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--config",
|
|
61
|
+
type=str,
|
|
62
|
+
default=default,
|
|
63
|
+
help="Path to testing.json config file (default: look near package directory)",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--reference-root",
|
|
67
|
+
type=str,
|
|
68
|
+
default=default,
|
|
69
|
+
help="Path to reference results root (default: <library>/Resources/ReferenceResults)",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--test-spec",
|
|
73
|
+
type=str,
|
|
74
|
+
default=default,
|
|
75
|
+
help="Path to test_spec.json (external test definitions)",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--dymola-interface",
|
|
79
|
+
type=str,
|
|
80
|
+
default=default,
|
|
81
|
+
help="Path to Dymola's Python interface archive (dymola.egg or dymola-*.whl) "
|
|
82
|
+
"or the directory containing it. Overrides auto-discovery.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_arg_parser() -> argparse.ArgumentParser:
|
|
87
|
+
"""Construct the top-level CLI parser. Pulled out of :func:`main` so
|
|
88
|
+
tests can introspect subcommands and flags without dispatching, and
|
|
89
|
+
so :func:`main` stays a thin parse-and-dispatch shell.
|
|
90
|
+
"""
|
|
91
|
+
from .. import __version__ as _dstf_version
|
|
92
|
+
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
prog="dstf",
|
|
95
|
+
description="Dynamic Systems Testing Framework — regression & unit testing for time-dependent system behavior",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--version",
|
|
99
|
+
"-V",
|
|
100
|
+
action="version",
|
|
101
|
+
version=f"%(prog)s {_dstf_version}",
|
|
102
|
+
help="Print the installed dstf version and exit.",
|
|
103
|
+
)
|
|
104
|
+
_add_global_flags(parser, subcommand=False)
|
|
105
|
+
|
|
106
|
+
# Inherited by every subparser so the global flags are accepted after the
|
|
107
|
+
# subcommand as well as before it.
|
|
108
|
+
global_parent = argparse.ArgumentParser(add_help=False)
|
|
109
|
+
_add_global_flags(global_parent, subcommand=True)
|
|
110
|
+
|
|
111
|
+
# Parent parsers for flag groups shared across subcommands. argparse
|
|
112
|
+
# merges these via parents=[...] on add_parser; each subcommand inherits
|
|
113
|
+
# only the groups that apply, keeping --help output identical to the
|
|
114
|
+
# pre-refactor inline form while eliminating duplication.
|
|
115
|
+
filter_parent = argparse.ArgumentParser(add_help=False)
|
|
116
|
+
filter_parent.add_argument(
|
|
117
|
+
"--filter",
|
|
118
|
+
type=str,
|
|
119
|
+
help="Glob, comma-separated list, or @file (one pattern per line) — "
|
|
120
|
+
"matches against model_id. Commas inside [] character classes are "
|
|
121
|
+
"part of the class, not separators (e.g. 'Test[A,B]*' is one pattern).",
|
|
122
|
+
)
|
|
123
|
+
filter_parent.add_argument("--package", type=str, help="Filter by package prefix")
|
|
124
|
+
|
|
125
|
+
compare_parent = argparse.ArgumentParser(add_help=False)
|
|
126
|
+
compare_parent.add_argument(
|
|
127
|
+
"--tolerance", type=float, help="Override comparison tolerance"
|
|
128
|
+
)
|
|
129
|
+
compare_parent.add_argument(
|
|
130
|
+
"--default-points",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="Use points mode (with empty points list) as the default for variables without an explicit ``mode`` override.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
report_parent = argparse.ArgumentParser(add_help=False)
|
|
136
|
+
report_parent.add_argument(
|
|
137
|
+
"--report-format",
|
|
138
|
+
choices=["console", "junit", "html"],
|
|
139
|
+
default="console",
|
|
140
|
+
help="Output format for test report",
|
|
141
|
+
)
|
|
142
|
+
report_parent.add_argument(
|
|
143
|
+
"--output",
|
|
144
|
+
"-o",
|
|
145
|
+
type=str,
|
|
146
|
+
default=None,
|
|
147
|
+
help="Destination for --report-format junit/html "
|
|
148
|
+
"(default: test-results.xml / test-report.html in the CWD). "
|
|
149
|
+
"Ignored by the console format.",
|
|
150
|
+
)
|
|
151
|
+
report_parent.add_argument(
|
|
152
|
+
"--report",
|
|
153
|
+
action="store_true",
|
|
154
|
+
help="Render per-test interactive deep-dive reports under "
|
|
155
|
+
"<work_dir>/reports/<test>/. The unified <work_dir>/dashboard.html "
|
|
156
|
+
"always renders regardless of this flag — it just doesn't link to "
|
|
157
|
+
"per-test pages without --report.",
|
|
158
|
+
)
|
|
159
|
+
report_parent.add_argument(
|
|
160
|
+
"--no-browser",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="Open no browser tab on the dashboard or a report page; the "
|
|
163
|
+
"file:// link is still printed.",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
167
|
+
|
|
168
|
+
# discover
|
|
169
|
+
subparsers.add_parser(
|
|
170
|
+
"discover",
|
|
171
|
+
parents=[global_parent, filter_parent],
|
|
172
|
+
help="Discover and list all test models",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# run
|
|
176
|
+
p_run = subparsers.add_parser(
|
|
177
|
+
"run",
|
|
178
|
+
parents=[global_parent, filter_parent, compare_parent, report_parent],
|
|
179
|
+
help="Run tests (simulate via the configured backend + compare against references)",
|
|
180
|
+
)
|
|
181
|
+
# review 2026-07-06 finding 61: review-then-accept (-i) and
|
|
182
|
+
# accept-everything (--accept) are mutually exclusive intents — make
|
|
183
|
+
# combining them a hard argparse error instead of silently letting
|
|
184
|
+
# --accept win and skipping the review the user asked for.
|
|
185
|
+
accept_or_review = p_run.add_mutually_exclusive_group()
|
|
186
|
+
accept_or_review.add_argument(
|
|
187
|
+
"-i",
|
|
188
|
+
"--interactive",
|
|
189
|
+
nargs="?",
|
|
190
|
+
const="all",
|
|
191
|
+
default=None,
|
|
192
|
+
metavar="FILTER",
|
|
193
|
+
type=_review_filter_arg, # review 2026-07-06 finding 58: parse-time check
|
|
194
|
+
help="Interactive review. Optional filter: failed, no-baseline, "
|
|
195
|
+
"warnings, sim-failed, passed (comma-separated). Default: all.",
|
|
196
|
+
)
|
|
197
|
+
accept_or_review.add_argument(
|
|
198
|
+
"--accept",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="Accept results as new baseline references",
|
|
201
|
+
)
|
|
202
|
+
p_run.add_argument("--backend", type=str, help="Backend name (e.g., 'Dymola 2025')")
|
|
203
|
+
p_run.add_argument(
|
|
204
|
+
"--backend-path",
|
|
205
|
+
type=str,
|
|
206
|
+
help="Absolute path to the backend's executable (overrides config)",
|
|
207
|
+
)
|
|
208
|
+
p_run.add_argument(
|
|
209
|
+
"--show-ide",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Show Dymola GUI instead of running headless",
|
|
212
|
+
)
|
|
213
|
+
p_run.add_argument("--work-dir", type=str, help="Working directory for output")
|
|
214
|
+
p_run.add_argument(
|
|
215
|
+
"--parallel",
|
|
216
|
+
type=int,
|
|
217
|
+
help="Number of concurrent SIMULATION workers (one simulator process each, where the backend drives one). "
|
|
218
|
+
"Note: on `compare` the same flag means report-rendering threads.",
|
|
219
|
+
)
|
|
220
|
+
p_run.add_argument(
|
|
221
|
+
"--batch-size",
|
|
222
|
+
type=int,
|
|
223
|
+
dest="batch_size",
|
|
224
|
+
help="(--batch only) Tests per Dymola session (default: all-per-worker). Small values (3-5) give better load balancing and crash isolation but reload the library more often.",
|
|
225
|
+
)
|
|
226
|
+
p_run.add_argument(
|
|
227
|
+
"--batch",
|
|
228
|
+
action="store_true",
|
|
229
|
+
help="Use the legacy batched script backend (Dymola .mos / OpenModelica subprocess) instead of the default persistent-worker mode. A backend whose persistent preflight fails (Dymola DymolaInterface or OMPython missing) aborts with a message pointing here — there is no silent fallback.",
|
|
230
|
+
)
|
|
231
|
+
p_run.add_argument(
|
|
232
|
+
"--timeout",
|
|
233
|
+
type=int,
|
|
234
|
+
default=None,
|
|
235
|
+
help="Per-test timeout in seconds (default: 60)",
|
|
236
|
+
)
|
|
237
|
+
p_run.add_argument(
|
|
238
|
+
"--merge",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="When used with --filter, expand the read/compare/report scope to "
|
|
241
|
+
"include all tests in the persistent batch manifest (not just the "
|
|
242
|
+
"filtered subset). Lets you rerun a few tests but still see a full "
|
|
243
|
+
"report covering the whole suite.",
|
|
244
|
+
)
|
|
245
|
+
p_run.add_argument(
|
|
246
|
+
"--rerun",
|
|
247
|
+
nargs="?",
|
|
248
|
+
const="failed",
|
|
249
|
+
default=None,
|
|
250
|
+
metavar="CATEGORIES",
|
|
251
|
+
type=_review_filter_arg, # review 2026-07-06 finding 58: parse-time check
|
|
252
|
+
help="Rerun tests selected by status from the prior run. Categories: "
|
|
253
|
+
"failed, no-baseline, warnings, sim-failed, passed (comma-separated). "
|
|
254
|
+
"Default: failed. Implies --merge.",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# compare
|
|
258
|
+
p_compare = subparsers.add_parser(
|
|
259
|
+
"compare",
|
|
260
|
+
parents=[global_parent, filter_parent, compare_parent, report_parent],
|
|
261
|
+
help="Compare last results against references",
|
|
262
|
+
)
|
|
263
|
+
p_compare.add_argument(
|
|
264
|
+
"--backend",
|
|
265
|
+
type=str,
|
|
266
|
+
help="Backend name (e.g., 'Dymola 2025'). Affects which "
|
|
267
|
+
"<reference_root>/<Backend>/<os>/ partition is read.",
|
|
268
|
+
)
|
|
269
|
+
p_compare.add_argument(
|
|
270
|
+
"--backend-path",
|
|
271
|
+
type=str,
|
|
272
|
+
help="Absolute path to the backend's executable (overrides config). "
|
|
273
|
+
"Mainly affects which backend identity is used for partition lookup.",
|
|
274
|
+
)
|
|
275
|
+
p_compare.add_argument(
|
|
276
|
+
"--work-dir", type=str, help="Working directory containing prior run outputs"
|
|
277
|
+
)
|
|
278
|
+
p_compare.add_argument(
|
|
279
|
+
"--parallel",
|
|
280
|
+
type=int,
|
|
281
|
+
help="Threads for --report RENDERING (Plotly serialization + JSON sidecar dump). "
|
|
282
|
+
"Comparison itself is fast; this is the post-comparison report suite. "
|
|
283
|
+
"Note: on `run` the same flag means simulation workers. Default: 1.",
|
|
284
|
+
)
|
|
285
|
+
p_compare.add_argument(
|
|
286
|
+
"--accept",
|
|
287
|
+
action="store_true",
|
|
288
|
+
help="Accept the last run's on-disk results as new baselines WITHOUT "
|
|
289
|
+
"re-simulating. Skips tests the run recorded as failed/timed_out.",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# author — the authoring tool (NiceGUI shell over the Session)
|
|
293
|
+
p_author = subparsers.add_parser(
|
|
294
|
+
"author",
|
|
295
|
+
parents=[global_parent, filter_parent],
|
|
296
|
+
help="Open the authoring tool on this project",
|
|
297
|
+
)
|
|
298
|
+
p_author.add_argument(
|
|
299
|
+
"--port",
|
|
300
|
+
type=int,
|
|
301
|
+
default=None,
|
|
302
|
+
help="Port to serve the shell on (default: 8080). It binds 127.0.0.1 only.",
|
|
303
|
+
)
|
|
304
|
+
p_author.add_argument(
|
|
305
|
+
"--no-browser",
|
|
306
|
+
action="store_true",
|
|
307
|
+
help="Serve without opening a browser tab (headless launch).",
|
|
308
|
+
)
|
|
309
|
+
p_author.add_argument(
|
|
310
|
+
"--work-dir", type=str, help="Working directory containing prior run outputs"
|
|
311
|
+
)
|
|
312
|
+
p_author.add_argument(
|
|
313
|
+
"--backend",
|
|
314
|
+
type=str,
|
|
315
|
+
help="Backend name (e.g., 'Dymola 2025'). Affects which "
|
|
316
|
+
"<reference_root>/<Backend>/<os>/ partition is read.",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# export
|
|
320
|
+
p_export = subparsers.add_parser(
|
|
321
|
+
"export",
|
|
322
|
+
parents=[global_parent],
|
|
323
|
+
help="Export reference data",
|
|
324
|
+
)
|
|
325
|
+
p_export.add_argument(
|
|
326
|
+
"--format",
|
|
327
|
+
choices=["json", "csv"],
|
|
328
|
+
default="json",
|
|
329
|
+
help="Export format",
|
|
330
|
+
)
|
|
331
|
+
p_export.add_argument("--output", type=str, help="Output file path")
|
|
332
|
+
|
|
333
|
+
# manifest
|
|
334
|
+
p_manifest = subparsers.add_parser(
|
|
335
|
+
"manifest", parents=[global_parent], help="Manage the test manifest"
|
|
336
|
+
)
|
|
337
|
+
p_manifest.add_argument(
|
|
338
|
+
"action",
|
|
339
|
+
choices=["show", "dump", "cleanup"],
|
|
340
|
+
help="'show': display all references and their status. "
|
|
341
|
+
"'dump': write ref ID to model ID mapping to work directory. "
|
|
342
|
+
"'cleanup': remove reference files with status 'obsolete' "
|
|
343
|
+
"(dry-run by default; pass --apply to delete).",
|
|
344
|
+
)
|
|
345
|
+
p_manifest.add_argument(
|
|
346
|
+
"--show-obsolete",
|
|
347
|
+
action="store_true",
|
|
348
|
+
help="Also show obsolete entries (show only)",
|
|
349
|
+
)
|
|
350
|
+
p_manifest.add_argument(
|
|
351
|
+
"--orphans",
|
|
352
|
+
action="store_true",
|
|
353
|
+
help="cleanup: target orphan batch_manifest entries (models no longer "
|
|
354
|
+
"in discovery) instead of obsolete reference files.",
|
|
355
|
+
)
|
|
356
|
+
p_manifest.add_argument(
|
|
357
|
+
"--apply",
|
|
358
|
+
action="store_true",
|
|
359
|
+
help="cleanup: actually delete. EVERY cleanup path is a dry-run "
|
|
360
|
+
"without this flag — it lists what it would remove and exits.",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# add
|
|
364
|
+
p_add = subparsers.add_parser(
|
|
365
|
+
"add", parents=[global_parent], help="Add a test to test_spec.json"
|
|
366
|
+
)
|
|
367
|
+
p_add.add_argument(
|
|
368
|
+
"model_id",
|
|
369
|
+
type=str,
|
|
370
|
+
help="Fully qualified Modelica model path (e.g., 'MyLib.Examples.Test')",
|
|
371
|
+
)
|
|
372
|
+
p_add.add_argument(
|
|
373
|
+
"--variables",
|
|
374
|
+
nargs="*",
|
|
375
|
+
default=None,
|
|
376
|
+
help="Variable patterns to track (e.g., 'pipe.T*' 'pump.m_flow'). "
|
|
377
|
+
"Omit for simulate-only.",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# spec-update
|
|
381
|
+
p_spec_update = subparsers.add_parser(
|
|
382
|
+
"spec-update",
|
|
383
|
+
parents=[global_parent],
|
|
384
|
+
help="Apply a JSON-Patch envelope to test_spec.json",
|
|
385
|
+
)
|
|
386
|
+
p_spec_update.add_argument(
|
|
387
|
+
"json_file",
|
|
388
|
+
type=str,
|
|
389
|
+
help="Path to the patch envelope (e.g. spec_patch.json from dstf author's Export)",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# companion — manage plot-only overlays (D66 baseline-role split)
|
|
393
|
+
p_companion = subparsers.add_parser(
|
|
394
|
+
"companion",
|
|
395
|
+
parents=[global_parent],
|
|
396
|
+
help="Manage plot-only companion references (external data shown as overlays)",
|
|
397
|
+
)
|
|
398
|
+
p_companion.add_argument(
|
|
399
|
+
"action",
|
|
400
|
+
choices=["add", "list", "freeze", "remove"],
|
|
401
|
+
help="'add': register an external file pointer. 'list': show companions. "
|
|
402
|
+
"'freeze': copy external data into ref storage. 'remove': deregister.",
|
|
403
|
+
)
|
|
404
|
+
p_companion.add_argument(
|
|
405
|
+
"model_id",
|
|
406
|
+
nargs="?",
|
|
407
|
+
help="Model ID (required for add/freeze/remove; optional for list)",
|
|
408
|
+
)
|
|
409
|
+
p_companion.add_argument(
|
|
410
|
+
"name", nargs="?", help="Companion name (required for add/freeze/remove)"
|
|
411
|
+
)
|
|
412
|
+
p_companion.add_argument(
|
|
413
|
+
"path", nargs="?", help="Path to data file (required for add)"
|
|
414
|
+
)
|
|
415
|
+
p_companion.add_argument(
|
|
416
|
+
"--format",
|
|
417
|
+
choices=["csv", "json"],
|
|
418
|
+
default=None,
|
|
419
|
+
help="Data format (auto-detected from extension if omitted)",
|
|
420
|
+
)
|
|
421
|
+
p_companion.add_argument(
|
|
422
|
+
"--map",
|
|
423
|
+
dest="variable_map",
|
|
424
|
+
action="append",
|
|
425
|
+
metavar="TEST_VAR=COMPANION_VAR",
|
|
426
|
+
default=None,
|
|
427
|
+
help="'add' only: which of the companion's variables draws on which "
|
|
428
|
+
"of the test's. Repeat per variable. Both names are checked against "
|
|
429
|
+
"the stored baseline and the companion file. Two test variables "
|
|
430
|
+
"may name one companion variable (one curve, drawn twice). "
|
|
431
|
+
"Without any --map the reporter falls back on matching spellings.",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# soft-check — manage advisory cross-check baselines (D66)
|
|
435
|
+
p_soft = subparsers.add_parser(
|
|
436
|
+
"soft-check",
|
|
437
|
+
parents=[global_parent],
|
|
438
|
+
help="Manage soft_check baselines (advisory cross-checks; warn-only scoring)",
|
|
439
|
+
)
|
|
440
|
+
p_soft.add_argument(
|
|
441
|
+
"action",
|
|
442
|
+
choices=["list", "remove"],
|
|
443
|
+
help="'list': show soft_checks. 'remove': deregister. "
|
|
444
|
+
"Note: there is no `soft-check add` — by design, soft_checks are "
|
|
445
|
+
"added only via `import-baseline` (D66 baseline-role split: "
|
|
446
|
+
"soft_checks come from another regression system's primary, not "
|
|
447
|
+
"from local user input).",
|
|
448
|
+
)
|
|
449
|
+
p_soft.add_argument("model_id", nargs="?")
|
|
450
|
+
p_soft.add_argument("name", nargs="?")
|
|
451
|
+
|
|
452
|
+
# import-baseline — import another system's primary as a soft_check (D66)
|
|
453
|
+
p_import = subparsers.add_parser(
|
|
454
|
+
"import-baseline",
|
|
455
|
+
parents=[global_parent],
|
|
456
|
+
help="Import another regression system's primary as a soft_check",
|
|
457
|
+
)
|
|
458
|
+
p_import.add_argument(
|
|
459
|
+
"model_id", type=str, help="Local model ID to attach the soft_check to"
|
|
460
|
+
)
|
|
461
|
+
p_import.add_argument(
|
|
462
|
+
"name",
|
|
463
|
+
type=str,
|
|
464
|
+
help="Soft_check name (how MetricTree leaves will reference it)",
|
|
465
|
+
)
|
|
466
|
+
p_import.add_argument(
|
|
467
|
+
# The dest must NOT be "source_path": that name is also the global
|
|
468
|
+
# --source-path option, and _build_config would treat the ref JSON
|
|
469
|
+
# as the library root (find_package_dir then iterdir()s a file —
|
|
470
|
+
# NotADirectoryError; found live during W-6, D103). metavar keeps
|
|
471
|
+
# the user-facing name.
|
|
472
|
+
"import_source",
|
|
473
|
+
metavar="source_path",
|
|
474
|
+
type=str,
|
|
475
|
+
help="Path to source reference JSON (flat ref file shape)",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# export-schema — emit JSON-Schema for test_spec.json (6.4.5)
|
|
479
|
+
p_schema = subparsers.add_parser(
|
|
480
|
+
"export-schema",
|
|
481
|
+
parents=[global_parent],
|
|
482
|
+
help="Emit a JSON-Schema describing the test_spec.json shape",
|
|
483
|
+
)
|
|
484
|
+
p_schema.add_argument(
|
|
485
|
+
"--output",
|
|
486
|
+
"-o",
|
|
487
|
+
type=str,
|
|
488
|
+
default=None,
|
|
489
|
+
help="Write schema to this file instead of stdout",
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# check-* — backend diagnostic commands (symmetric across Dymola / OM / Julia)
|
|
493
|
+
subparsers.add_parser(
|
|
494
|
+
"check-dymola",
|
|
495
|
+
parents=[global_parent],
|
|
496
|
+
help="Locate and load Dymola's Python interface; report what was found",
|
|
497
|
+
)
|
|
498
|
+
subparsers.add_parser(
|
|
499
|
+
"check-openmodelica",
|
|
500
|
+
parents=[global_parent],
|
|
501
|
+
help="Probe OMPython + the `omc` binary; report whether the OM stack is ready",
|
|
502
|
+
)
|
|
503
|
+
subparsers.add_parser(
|
|
504
|
+
"check-julia",
|
|
505
|
+
parents=[global_parent],
|
|
506
|
+
help="Probe the `julia` binary on PATH; report version and readiness",
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return parser
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# Subcommand → handler dispatch table. Lives at module scope so tests can
|
|
513
|
+
# assert the registered commands without re-running `main()`. Update both
|
|
514
|
+
# the parser (in :func:`build_arg_parser`) and this dict when adding a
|
|
515
|
+
# subcommand — symmetry is checked by :data:`_COMMANDS_PARSER_PARITY` test.
|
|
516
|
+
_COMMANDS = {
|
|
517
|
+
"discover": cmd_discover,
|
|
518
|
+
"run": cmd_run,
|
|
519
|
+
"compare": cmd_compare,
|
|
520
|
+
"author": cmd_author,
|
|
521
|
+
"export": cmd_export,
|
|
522
|
+
"manifest": cmd_manifest,
|
|
523
|
+
"add": cmd_add,
|
|
524
|
+
"spec-update": cmd_spec_update,
|
|
525
|
+
"companion": cmd_companion,
|
|
526
|
+
"soft-check": cmd_soft_check,
|
|
527
|
+
"import-baseline": cmd_import_baseline,
|
|
528
|
+
"export-schema": cmd_export_schema,
|
|
529
|
+
"check-dymola": cmd_check_dymola,
|
|
530
|
+
"check-openmodelica": cmd_check_openmodelica,
|
|
531
|
+
"check-julia": cmd_check_julia,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def main(argv: list[str] | None = None) -> int:
|
|
536
|
+
parser = build_arg_parser()
|
|
537
|
+
args = parser.parse_args(argv)
|
|
538
|
+
if args.command is None:
|
|
539
|
+
parser.print_help()
|
|
540
|
+
return 1
|
|
541
|
+
try:
|
|
542
|
+
# Before dispatch so every subcommand sees the same registries —
|
|
543
|
+
# a plug-in backend must be resolvable by `discover`, `run` and
|
|
544
|
+
# `export-schema` alike. A broken plug-in raises ConfigError and
|
|
545
|
+
# lands on the clean exit-2 path below.
|
|
546
|
+
from ..plugins import load_plugins
|
|
547
|
+
|
|
548
|
+
load_plugins()
|
|
549
|
+
return _COMMANDS[args.command](args)
|
|
550
|
+
except ConfigError as exc:
|
|
551
|
+
# review 2026-07-06 leftover A: a testing.json problem (malformed,
|
|
552
|
+
# unreadable, missing explicit --config) is a user error — print one
|
|
553
|
+
# clean line and exit 2 (argparse's usage-error code), never a
|
|
554
|
+
# traceback.
|
|
555
|
+
print(f"Config error: {exc}", file=sys.stderr)
|
|
556
|
+
return 2
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def main_entry() -> None:
|
|
560
|
+
"""Entry point for the console_scripts wrapper."""
|
|
561
|
+
sys.exit(main())
|