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/diag_cmds.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Query and diagnostic commands: discover, export, manifest, check-*."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..config import Config
|
|
7
|
+
from ..discovery.test_registry import TestModel, discover_tests
|
|
8
|
+
from ..discovery.unit_tests import unit_test_variables
|
|
9
|
+
from ..pipeline import write_id_mapping
|
|
10
|
+
from .common import (
|
|
11
|
+
_build_config,
|
|
12
|
+
_filter_tests,
|
|
13
|
+
_find_orphan_manifest_entries,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _vars_text(test: TestModel) -> str:
|
|
18
|
+
"""What a test tracks, as far as discovery knows it: the count of its
|
|
19
|
+
UnitTests variables, and its ``variables`` entries as PATTERNS — `2`,
|
|
20
|
+
`1 pattern`, `2 + 3 patterns`. A pattern names no variable until it is
|
|
21
|
+
matched against a result; counted as variables they read `0`."""
|
|
22
|
+
declared = len(unit_test_variables(test))
|
|
23
|
+
patterns = len(test.variable_patterns)
|
|
24
|
+
if not patterns:
|
|
25
|
+
return str(declared)
|
|
26
|
+
text = f"{patterns} pattern{'' if patterns == 1 else 's'}"
|
|
27
|
+
return f"{declared} + {text}" if declared else text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _method_text(test: TestModel, config: Config) -> str:
|
|
31
|
+
"""The solver the test names, or `—` where it names none and the
|
|
32
|
+
framework's default would not be true: ``Dassl`` is what a Modelica tool
|
|
33
|
+
is handed, while FMPy maps it to its own solver and a Julia or Python
|
|
34
|
+
source has no solver setting at all."""
|
|
35
|
+
defaulted = test.field_sources.get("method") == "default"
|
|
36
|
+
if defaulted and config.source_type != "modelica":
|
|
37
|
+
return "—"
|
|
38
|
+
return str(test.method)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_discover(args: argparse.Namespace) -> int:
|
|
42
|
+
"""Discover and list all test models."""
|
|
43
|
+
config = _build_config(args)
|
|
44
|
+
tests = discover_tests(config)
|
|
45
|
+
tests = _filter_tests(tests, args.filter, args.package)
|
|
46
|
+
|
|
47
|
+
if not tests:
|
|
48
|
+
print("No tests found.")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
# Print summary table. Every cell is something true before any run:
|
|
52
|
+
# see `_vars_text` and `_method_text`.
|
|
53
|
+
print(
|
|
54
|
+
f"{'Model ID':<90} {'Vars':>16} {'StopTime':>10} {'Method':<10} {'Source':<10}"
|
|
55
|
+
)
|
|
56
|
+
print("-" * 142)
|
|
57
|
+
for t in tests:
|
|
58
|
+
stop = f"{t.stop_time:g}"
|
|
59
|
+
print(
|
|
60
|
+
f"{t.model_id:<90} {_vars_text(t):>16} {stop:>10} "
|
|
61
|
+
f"{_method_text(t, config):<10} {t.source:<10}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
print(f"\nTotal: {len(tests)} tests")
|
|
65
|
+
|
|
66
|
+
print(f" UnitTests variables: {sum(len(unit_test_variables(t)) for t in tests)}")
|
|
67
|
+
print(
|
|
68
|
+
f" Variable patterns: {sum(len(t.variable_patterns) for t in tests)} "
|
|
69
|
+
f"(each is matched against its test's result after the run)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
by_source = {}
|
|
73
|
+
for t in tests:
|
|
74
|
+
by_source[t.source] = by_source.get(t.source, 0) + 1
|
|
75
|
+
for source, count in sorted(by_source.items()):
|
|
76
|
+
print(f" Source '{source}': {count}")
|
|
77
|
+
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_export(args: argparse.Namespace) -> int:
|
|
82
|
+
"""Export reference data."""
|
|
83
|
+
config = _build_config(args)
|
|
84
|
+
from ..storage.reference_store import ReferenceStore
|
|
85
|
+
|
|
86
|
+
store = ReferenceStore(config)
|
|
87
|
+
|
|
88
|
+
# review 2026-07-06 finding 60: --output arrives as a string — coerce to
|
|
89
|
+
# Path (the ReferenceStore export APIs call .parent on it).
|
|
90
|
+
if args.format == "csv":
|
|
91
|
+
out = Path(args.output) if args.output else (config.work_dir / "references.csv")
|
|
92
|
+
store.export_csv(out)
|
|
93
|
+
print(f"Exported to {out}")
|
|
94
|
+
else:
|
|
95
|
+
out = (
|
|
96
|
+
Path(args.output) if args.output else (config.work_dir / "references.json")
|
|
97
|
+
)
|
|
98
|
+
store.export_json(out)
|
|
99
|
+
print(f"Exported to {out}")
|
|
100
|
+
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_manifest(args: argparse.Namespace) -> int:
|
|
105
|
+
"""Show and manage test references."""
|
|
106
|
+
config = _build_config(args)
|
|
107
|
+
from ..storage.reference_store import ReferenceStore
|
|
108
|
+
|
|
109
|
+
store = ReferenceStore(config)
|
|
110
|
+
action = args.action
|
|
111
|
+
|
|
112
|
+
if action == "show":
|
|
113
|
+
all_tests = store.index.all_tests()
|
|
114
|
+
active = {tid: e for tid, e in all_tests.items() if e["status"] != "obsolete"}
|
|
115
|
+
obsolete = {tid: e for tid, e in all_tests.items() if e["status"] == "obsolete"}
|
|
116
|
+
skipped = {tid: e for tid, e in all_tests.items() if e["status"] == "skip"}
|
|
117
|
+
|
|
118
|
+
print(f"References: {config.reference_dir}")
|
|
119
|
+
print(
|
|
120
|
+
f"Active: {len(active)} Skip: {len(skipped)} Obsolete: {len(obsolete)}\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if active:
|
|
124
|
+
print(f"{'ID':>6} {'Status':<10} {'Model ID'}")
|
|
125
|
+
print("-" * 90)
|
|
126
|
+
for tid in sorted(active):
|
|
127
|
+
entry = active[tid]
|
|
128
|
+
print(f"{tid:>6} {entry['status']:<10} {entry['model_id']}")
|
|
129
|
+
|
|
130
|
+
if skipped:
|
|
131
|
+
print("\nSkipped:")
|
|
132
|
+
for tid in sorted(skipped):
|
|
133
|
+
print(f"{tid:>6} {skipped[tid]['model_id']}")
|
|
134
|
+
|
|
135
|
+
if obsolete and args.show_obsolete:
|
|
136
|
+
print("\nObsolete:")
|
|
137
|
+
for tid in sorted(obsolete):
|
|
138
|
+
print(f"{tid:>6} {obsolete[tid]['model_id']}")
|
|
139
|
+
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
elif action == "dump":
|
|
143
|
+
write_id_mapping(store, config)
|
|
144
|
+
mapping_path = config.work_dir / "reference_manifest.json"
|
|
145
|
+
print(f"Written to {mapping_path}")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
elif action == "cleanup":
|
|
149
|
+
# Dry-run is the universal default for destructive paths: `cleanup`
|
|
150
|
+
# lists what it would remove and exits 0 unless --apply is given.
|
|
151
|
+
# The --orphans path always worked this way; the obsolete-reference
|
|
152
|
+
# path used to delete on sight.
|
|
153
|
+
apply = getattr(args, "apply", False)
|
|
154
|
+
if getattr(args, "orphans", False):
|
|
155
|
+
return _cleanup_orphans(config, apply=apply)
|
|
156
|
+
return _cleanup_obsolete(store, apply=apply)
|
|
157
|
+
|
|
158
|
+
return 1
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cleanup_obsolete(store, apply: bool) -> int:
|
|
162
|
+
"""List (or, with apply=True, delete) reference files marked obsolete."""
|
|
163
|
+
obsolete = store.list_obsolete()
|
|
164
|
+
if not obsolete:
|
|
165
|
+
print("No obsolete references to clean up.")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
print(f"Found {len(obsolete)} obsolete reference files:")
|
|
169
|
+
for test_id, model_id, ref_file in obsolete:
|
|
170
|
+
print(f" {ref_file.name} {model_id or f'ref_{test_id}'}")
|
|
171
|
+
|
|
172
|
+
if not apply:
|
|
173
|
+
print("\nDry run. Re-run with --apply to actually remove.")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
removed = store.cleanup_obsolete()
|
|
177
|
+
print(f"\nRemoved {removed} obsolete reference files.")
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _cleanup_orphans(config, apply: bool) -> int:
|
|
182
|
+
"""List or prune manifest entries (and their on-disk dirs) for models
|
|
183
|
+
no longer in discover_tests. Dry-run by default; pass apply=True to delete.
|
|
184
|
+
"""
|
|
185
|
+
all_tests = discover_tests(config)
|
|
186
|
+
orphans = _find_orphan_manifest_entries(config, all_tests)
|
|
187
|
+
if not orphans:
|
|
188
|
+
print("No orphan manifest entries.")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
report_dir = config.work_dir / "reports"
|
|
192
|
+
print(f"Found {len(orphans)} orphan manifest entries:")
|
|
193
|
+
for tk, model_id in sorted(orphans.items()):
|
|
194
|
+
work = config.work_dir / tk
|
|
195
|
+
# Try both possible report-dir names
|
|
196
|
+
rep_test = report_dir / tk
|
|
197
|
+
rep_ref = None
|
|
198
|
+
from ..storage.reference_store import ReferenceStore
|
|
199
|
+
|
|
200
|
+
store = ReferenceStore(config)
|
|
201
|
+
ref_id = store.index.get_id(model_id)
|
|
202
|
+
if ref_id:
|
|
203
|
+
rep_ref = report_dir / f"ref_{ref_id}"
|
|
204
|
+
bits = []
|
|
205
|
+
if work.exists():
|
|
206
|
+
bits.append(f"work_dir={tk}/")
|
|
207
|
+
if rep_test.exists():
|
|
208
|
+
bits.append(f"reports/{tk}/")
|
|
209
|
+
if rep_ref and rep_ref.exists():
|
|
210
|
+
bits.append(f"reports/ref_{ref_id}/")
|
|
211
|
+
print(f" {tk} {model_id} [{', '.join(bits) or 'manifest only'}]")
|
|
212
|
+
|
|
213
|
+
if not apply:
|
|
214
|
+
print("\nDry run. Re-run with --apply to actually remove.")
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
import shutil
|
|
218
|
+
|
|
219
|
+
from ..backends import BatchManifest
|
|
220
|
+
|
|
221
|
+
manifest_path = config.work_dir / "batch_manifest.json"
|
|
222
|
+
manifest = BatchManifest.load(manifest_path)
|
|
223
|
+
n_dirs = 0
|
|
224
|
+
for tk, model_id in orphans.items():
|
|
225
|
+
for candidate in [
|
|
226
|
+
config.work_dir / tk,
|
|
227
|
+
report_dir / tk,
|
|
228
|
+
]:
|
|
229
|
+
if candidate.exists():
|
|
230
|
+
shutil.rmtree(candidate)
|
|
231
|
+
n_dirs += 1
|
|
232
|
+
from ..storage.reference_store import ReferenceStore
|
|
233
|
+
|
|
234
|
+
store = ReferenceStore(config)
|
|
235
|
+
ref_id = store.index.get_id(model_id)
|
|
236
|
+
if ref_id:
|
|
237
|
+
rep_ref = report_dir / f"ref_{ref_id}"
|
|
238
|
+
if rep_ref.exists():
|
|
239
|
+
shutil.rmtree(rep_ref)
|
|
240
|
+
n_dirs += 1
|
|
241
|
+
manifest.manifest.pop(tk, None)
|
|
242
|
+
manifest.save()
|
|
243
|
+
print(f"\nRemoved {len(orphans)} manifest entries and {n_dirs} directories.")
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cmd_check_dymola(args: argparse.Namespace) -> int:
|
|
248
|
+
"""Diagnose discovery of Dymola's Python interface."""
|
|
249
|
+
from ..backends.dymola.interface_loader import describe_dymola_interface
|
|
250
|
+
|
|
251
|
+
override = (
|
|
252
|
+
Path(args.dymola_interface) if getattr(args, "dymola_interface", None) else None
|
|
253
|
+
)
|
|
254
|
+
info = describe_dymola_interface(override)
|
|
255
|
+
|
|
256
|
+
print("Dymola Python interface diagnostic")
|
|
257
|
+
print("==================================")
|
|
258
|
+
print(f" Archive found: {info['archive'] or '(none)'}")
|
|
259
|
+
if info["format"]:
|
|
260
|
+
print(f" Format: .{info['format']}")
|
|
261
|
+
print(f" sys.path entry: {info['sys_path_entry'] or '(none)'}")
|
|
262
|
+
print(f" import ok: {info['import_ok']}")
|
|
263
|
+
if info.get("dymola_interface_module"):
|
|
264
|
+
print(f" Module: {info['dymola_interface_module']}")
|
|
265
|
+
if info["error"]:
|
|
266
|
+
print(f" Error: {info['error']}")
|
|
267
|
+
print()
|
|
268
|
+
print("Searched roots:")
|
|
269
|
+
for r in info["search_roots"]:
|
|
270
|
+
print(f" - {r}")
|
|
271
|
+
if info["discovered_dirs"]:
|
|
272
|
+
print("Discovered install dirs (newest first):")
|
|
273
|
+
for d in info["discovered_dirs"]:
|
|
274
|
+
print(f" - {d}")
|
|
275
|
+
|
|
276
|
+
if info["import_ok"]:
|
|
277
|
+
print("\nOK — ready to use the Dymola Python interface.")
|
|
278
|
+
return 0
|
|
279
|
+
print("\nFAIL — fix one of:")
|
|
280
|
+
print(" 1. Pass --dymola-interface <path>")
|
|
281
|
+
print(" 2. Set DYMOLA_INTERFACE_PATH env var")
|
|
282
|
+
return 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_check_openmodelica(args: argparse.Namespace) -> int:
|
|
286
|
+
"""Diagnose OMPython availability + the `omc` binary."""
|
|
287
|
+
import shutil as _shutil
|
|
288
|
+
|
|
289
|
+
from ..backends.openmodelica.session_loader import describe_om_session
|
|
290
|
+
|
|
291
|
+
info = describe_om_session()
|
|
292
|
+
omc_path = _shutil.which("omc")
|
|
293
|
+
|
|
294
|
+
print("OpenModelica diagnostic")
|
|
295
|
+
print("=======================")
|
|
296
|
+
print(f" OMPython installed: {info['ompython_installed']}")
|
|
297
|
+
print(f" OMPython version: {info['version'] or '(unknown)'}")
|
|
298
|
+
print(f" Import ok: {info['import_ok']}")
|
|
299
|
+
if info["error"]:
|
|
300
|
+
print(f" Error: {info['error']}")
|
|
301
|
+
print(f" omc binary: {omc_path or '(not on PATH)'}")
|
|
302
|
+
|
|
303
|
+
ok = info["import_ok"] and bool(omc_path)
|
|
304
|
+
if ok:
|
|
305
|
+
print("\nOK — persistent-worker OpenModelica is ready.")
|
|
306
|
+
return 0
|
|
307
|
+
print("\nFAIL — fix one of:")
|
|
308
|
+
if not info["import_ok"]:
|
|
309
|
+
print(" - Install OMPython: uv sync --extra om --locked --inexact")
|
|
310
|
+
print(" (or reinstall dstf with the [om] extra)")
|
|
311
|
+
if not omc_path:
|
|
312
|
+
print(" - Install OpenModelica and ensure `omc` is on PATH")
|
|
313
|
+
print(
|
|
314
|
+
"Or run with --batch to use the omc-script fallback (still needs omc on PATH)."
|
|
315
|
+
)
|
|
316
|
+
return 1
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_check_julia(args: argparse.Namespace) -> int:
|
|
320
|
+
"""Diagnose the `julia` binary on PATH."""
|
|
321
|
+
import shutil as _shutil
|
|
322
|
+
import subprocess as _subprocess
|
|
323
|
+
|
|
324
|
+
julia_path = _shutil.which("julia")
|
|
325
|
+
print("Julia diagnostic")
|
|
326
|
+
print("================")
|
|
327
|
+
print(f" julia binary: {julia_path or '(not on PATH)'}")
|
|
328
|
+
if not julia_path:
|
|
329
|
+
print(
|
|
330
|
+
"\nFAIL — install Julia from https://julialang.org/downloads/ "
|
|
331
|
+
"(or via `juliaup`)."
|
|
332
|
+
)
|
|
333
|
+
return 1
|
|
334
|
+
try:
|
|
335
|
+
out = _subprocess.run(
|
|
336
|
+
[julia_path, "--version"],
|
|
337
|
+
capture_output=True,
|
|
338
|
+
text=True,
|
|
339
|
+
timeout=10,
|
|
340
|
+
)
|
|
341
|
+
print(f" version: {out.stdout.strip() or '(no output)'}")
|
|
342
|
+
except Exception as exc: # noqa: BLE001
|
|
343
|
+
print(f" version probe failed: {exc}")
|
|
344
|
+
return 1
|
|
345
|
+
print("\nOK — julia is invocable.")
|
|
346
|
+
print(
|
|
347
|
+
"Note: first run on a project pays a multi-minute MTK / OrdinaryDiffEq "
|
|
348
|
+
"JIT compile (subsequent runs hit the cache)."
|
|
349
|
+
)
|
|
350
|
+
return 0
|