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.
Files changed (185) hide show
  1. dstf/__init__.py +41 -0
  2. dstf/__main__.py +10 -0
  3. dstf/authoring/__init__.py +13 -0
  4. dstf/authoring/builtin/__init__.py +32 -0
  5. dstf/authoring/builtin/accept.py +205 -0
  6. dstf/authoring/builtin/edit_criteria.py +479 -0
  7. dstf/authoring/builtin/edit_family.py +228 -0
  8. dstf/authoring/builtin/edit_on_plot.py +1354 -0
  9. dstf/authoring/builtin/edit_tree.py +1399 -0
  10. dstf/authoring/builtin/enqueue.py +144 -0
  11. dstf/authoring/capabilities.py +463 -0
  12. dstf/authoring/errors.py +49 -0
  13. dstf/authoring/family_rows.py +526 -0
  14. dstf/authoring/forms.py +631 -0
  15. dstf/authoring/layout.py +104 -0
  16. dstf/authoring/live_rows.py +337 -0
  17. dstf/authoring/live_runs.py +385 -0
  18. dstf/authoring/needle.py +127 -0
  19. dstf/authoring/picker_rows.py +92 -0
  20. dstf/authoring/queue_rows.py +354 -0
  21. dstf/authoring/session.py +3016 -0
  22. dstf/authoring/shell/__init__.py +9 -0
  23. dstf/authoring/shell/actions.py +702 -0
  24. dstf/authoring/shell/app.py +53 -0
  25. dstf/authoring/shell/drawer_controls.py +190 -0
  26. dstf/authoring/shell/family_fold.py +468 -0
  27. dstf/authoring/shell/figure_hook.py +249 -0
  28. dstf/authoring/shell/grips.js +1000 -0
  29. dstf/authoring/shell/header.py +783 -0
  30. dstf/authoring/shell/live_view.py +93 -0
  31. dstf/authoring/shell/picker.py +267 -0
  32. dstf/authoring/shell/placing.py +122 -0
  33. dstf/authoring/shell/plot_card.py +1171 -0
  34. dstf/authoring/shell/queue_panel.py +433 -0
  35. dstf/authoring/shell/rail.py +593 -0
  36. dstf/authoring/shell/shell.py +528 -0
  37. dstf/authoring/shell/stats_card.py +501 -0
  38. dstf/authoring/shell/strip.py +204 -0
  39. dstf/authoring/shell/style.py +721 -0
  40. dstf/authoring/shell/suite.py +277 -0
  41. dstf/authoring/shell/suite_grid.js +414 -0
  42. dstf/authoring/shell/suite_grid.py +373 -0
  43. dstf/authoring/shell/tab_theme.py +110 -0
  44. dstf/authoring/shell/tabstate.py +146 -0
  45. dstf/authoring/shell/test_list.py +179 -0
  46. dstf/authoring/shell/test_page.py +1404 -0
  47. dstf/authoring/shell/tree_card.py +1171 -0
  48. dstf/authoring/shell/variables_drawer.py +589 -0
  49. dstf/authoring/shell/widgets.py +188 -0
  50. dstf/authoring/situation.py +287 -0
  51. dstf/authoring/spec_document.py +215 -0
  52. dstf/authoring/spec_preview.py +83 -0
  53. dstf/authoring/suite_rows.py +99 -0
  54. dstf/authoring/tree_move.py +516 -0
  55. dstf/authoring/tree_pointer.py +33 -0
  56. dstf/authoring/tree_text.py +61 -0
  57. dstf/authoring/typed_number.js +64 -0
  58. dstf/authoring/typed_number.py +76 -0
  59. dstf/authoring/unscorable.py +141 -0
  60. dstf/authoring/variable_rows.py +216 -0
  61. dstf/backends/__init__.py +283 -0
  62. dstf/backends/base.py +1117 -0
  63. dstf/backends/common/__init__.py +1 -0
  64. dstf/backends/common/json_driver.py +484 -0
  65. dstf/backends/common/live_mat.py +369 -0
  66. dstf/backends/common/mat_reader.py +632 -0
  67. dstf/backends/common/proc_kill.py +238 -0
  68. dstf/backends/common/proc_output.py +20 -0
  69. dstf/backends/common/result_files.py +109 -0
  70. dstf/backends/common/result_variables.py +329 -0
  71. dstf/backends/common/statistics.py +39 -0
  72. dstf/backends/cross_backend.py +193 -0
  73. dstf/backends/dymola/__init__.py +5 -0
  74. dstf/backends/dymola/backend.py +776 -0
  75. dstf/backends/dymola/interface_loader.py +241 -0
  76. dstf/backends/dymola/log_parser.py +396 -0
  77. dstf/backends/dymola/persistent_backend.py +949 -0
  78. dstf/backends/fmpy/__init__.py +11 -0
  79. dstf/backends/fmpy/backend.py +443 -0
  80. dstf/backends/julia/__init__.py +27 -0
  81. dstf/backends/julia/backend.py +156 -0
  82. dstf/backends/julia/persistent_backend.py +541 -0
  83. dstf/backends/julia/run_persistent.jl +167 -0
  84. dstf/backends/julia/run_test.jl +146 -0
  85. dstf/backends/openmodelica/__init__.py +21 -0
  86. dstf/backends/openmodelica/backend.py +368 -0
  87. dstf/backends/openmodelica/init_xml.py +76 -0
  88. dstf/backends/openmodelica/log_parser.py +119 -0
  89. dstf/backends/openmodelica/mos_generator.py +228 -0
  90. dstf/backends/openmodelica/persistent_backend.py +1045 -0
  91. dstf/backends/openmodelica/session_loader.py +63 -0
  92. dstf/backends/persistent.py +1811 -0
  93. dstf/backends/pool.py +602 -0
  94. dstf/backends/progress.py +395 -0
  95. dstf/backends/python/__init__.py +11 -0
  96. dstf/backends/python/backend.py +141 -0
  97. dstf/backends/python/run_test.py +141 -0
  98. dstf/backends/run_metadata.py +103 -0
  99. dstf/backends/types.py +310 -0
  100. dstf/cli/__init__.py +12 -0
  101. dstf/cli/author_cmd.py +61 -0
  102. dstf/cli/baseline_cmds.py +341 -0
  103. dstf/cli/common.py +231 -0
  104. dstf/cli/diag_cmds.py +350 -0
  105. dstf/cli/main.py +561 -0
  106. dstf/cli/review.py +398 -0
  107. dstf/cli/run_cmds.py +589 -0
  108. dstf/cli/spec_cmds.py +123 -0
  109. dstf/comparison/__init__.py +13 -0
  110. dstf/comparison/algorithms.py +2127 -0
  111. dstf/comparison/builtins.py +291 -0
  112. dstf/comparison/comparator.py +1189 -0
  113. dstf/comparison/geometry.py +164 -0
  114. dstf/comparison/handles.py +1194 -0
  115. dstf/comparison/metric_tree.py +332 -0
  116. dstf/comparison/mode_schema.py +176 -0
  117. dstf/comparison/modes.py +788 -0
  118. dstf/comparison/registry.py +230 -0
  119. dstf/comparison/seeds.py +281 -0
  120. dstf/comparison/sides.py +158 -0
  121. dstf/comparison/snap.py +62 -0
  122. dstf/comparison/strict_keys.py +246 -0
  123. dstf/comparison/summaries.py +354 -0
  124. dstf/comparison/tree_eval.py +868 -0
  125. dstf/comparison/tree_spec.py +678 -0
  126. dstf/comparison/types.py +194 -0
  127. dstf/comparison/validator.py +232 -0
  128. dstf/config.py +815 -0
  129. dstf/discovery/__init__.py +0 -0
  130. dstf/discovery/json_recognizer.py +702 -0
  131. dstf/discovery/library_state.py +248 -0
  132. dstf/discovery/mo_parser.py +794 -0
  133. dstf/discovery/patch_apply.py +308 -0
  134. dstf/discovery/patch_envelope.py +63 -0
  135. dstf/discovery/plots.py +318 -0
  136. dstf/discovery/recognizer.py +103 -0
  137. dstf/discovery/scan_cache.py +154 -0
  138. dstf/discovery/spec_parser.py +538 -0
  139. dstf/discovery/spec_shape.py +122 -0
  140. dstf/discovery/spec_update.py +493 -0
  141. dstf/discovery/test_registry.py +488 -0
  142. dstf/discovery/unit_tests.py +95 -0
  143. dstf/figures/__init__.py +14 -0
  144. dstf/figures/assemble.py +48 -0
  145. dstf/figures/decimate.py +150 -0
  146. dstf/figures/families.py +289 -0
  147. dstf/figures/palette.py +170 -0
  148. dstf/figures/plotly.py +1413 -0
  149. dstf/figures/view.py +233 -0
  150. dstf/pipeline.py +435 -0
  151. dstf/plugins.py +104 -0
  152. dstf/reporting/__init__.py +0 -0
  153. dstf/reporting/console_report.py +187 -0
  154. dstf/reporting/context_builder.py +979 -0
  155. dstf/reporting/dashboard_render.py +534 -0
  156. dstf/reporting/junit_report.py +192 -0
  157. dstf/reporting/overlay_loader.py +708 -0
  158. dstf/reporting/report_suite.py +1059 -0
  159. dstf/reporting/rows.py +458 -0
  160. dstf/reporting/schema_export.py +413 -0
  161. dstf/reporting/stats_rows.py +321 -0
  162. dstf/reporting/status.py +388 -0
  163. dstf/reporting/templates/axes.js +61 -0
  164. dstf/reporting/templates/dashboard.html +284 -0
  165. dstf/reporting/templates/dashboard.js +604 -0
  166. dstf/reporting/templates/filters.js +100 -0
  167. dstf/reporting/templates/interactive.html +420 -0
  168. dstf/reporting/templates/report.js +116 -0
  169. dstf/reporting/templates/vendor/plotly.min.js +8 -0
  170. dstf/reporting/theme.py +324 -0
  171. dstf/reporting/tree_rows.py +73 -0
  172. dstf/reporting/verdicts.py +124 -0
  173. dstf/storage/__init__.py +0 -0
  174. dstf/storage/accept.py +216 -0
  175. dstf/storage/baseline.py +43 -0
  176. dstf/storage/batch_manifest.py +204 -0
  177. dstf/storage/ref_index.py +295 -0
  178. dstf/storage/reference_store.py +661 -0
  179. dstf/storage/roles.py +576 -0
  180. dstf/storage/run_stats.py +85 -0
  181. dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
  182. dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
  183. dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
  184. dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
  185. dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,283 @@
1
+ """Backends. Concrete today: Dymola, FMPy, OpenModelica, Julia, Python. Pluggable via ``@register``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..storage.batch_manifest import BatchManifest
8
+ from .base import Backend
9
+ from .types import (
10
+ Capability,
11
+ TestResult,
12
+ TestRunResult,
13
+ VariableResult,
14
+ resolve_variable_patterns,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from ..config import Config
19
+
20
+ __all__ = [
21
+ "Backend",
22
+ "BackendUnavailableError",
23
+ "VariableResult",
24
+ "TestResult",
25
+ "TestRunResult",
26
+ "BatchManifest",
27
+ "resolve_variable_patterns",
28
+ "get_backend",
29
+ "get_backend_class",
30
+ "select_backend",
31
+ "dataset_kind_for",
32
+ ]
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Backend registry
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _REGISTRY: dict[str, type[Backend]] = {}
40
+
41
+
42
+ def _validate_capabilities(cls: type[Backend], name: str) -> None:
43
+ """Assert declared :class:`Capability` flags have matching method overrides.
44
+
45
+ A capability declaration is a promise the framework can rely on. Without
46
+ this check, a backend can claim ``FMU_EXPORT`` and not override
47
+ :meth:`export_fmu` — the framework gates on the flag, calls the method,
48
+ and gets the base ``NotImplementedError`` at runtime instead of at
49
+ registration. We catch this drift at import time so the typo or
50
+ copy-paste-and-forget can't survive past `pytest --collect-only`.
51
+
52
+ Every capability has a load-bearing method, and all four are checked:
53
+
54
+ - ``FMU_EXPORT`` → ``export_fmu`` must be overridden.
55
+ - ``BATCH_FALLBACK`` → either ``run_tests`` or ``run_single_test`` must
56
+ be overridden (a backend can ship its own batched orchestration *or*
57
+ use the default per-test loop).
58
+ - ``PERSISTENT_WORKERS`` → ``persistent_backend_cls`` must be overridden
59
+ to return a non-None class.
60
+ - ``LIVE_RESULTS`` → ``live_result_path`` must be overridden.
61
+ - ``CANCEL`` → ``cancel_test`` must be overridden (a persistent variant's
62
+ cancel is its worker's, and is covered by tests like its other flags).
63
+
64
+ Only ``@register``-decorated classes are checked. Persistent variants
65
+ are reached via ``persistent_backend_cls``, not the registry, so their
66
+ declarations are covered by tests instead (see
67
+ ``tests/test_capability_gating.py::TestFmuExportGate``).
68
+ """
69
+ caps = cls.capabilities
70
+ if Capability.FMU_EXPORT in caps and cls.export_fmu is Backend.export_fmu:
71
+ raise TypeError(
72
+ f"@register('{name}'): {cls.__name__} declares "
73
+ f"Capability.FMU_EXPORT but does not override export_fmu(). "
74
+ f"Either implement the method or drop the capability flag."
75
+ )
76
+ if Capability.CANCEL in caps and cls.cancel_test is Backend.cancel_test:
77
+ raise TypeError(
78
+ f"@register('{name}'): {cls.__name__} declares "
79
+ f"Capability.CANCEL but does not override cancel_test(). "
80
+ f"Either implement the method or drop the capability flag."
81
+ )
82
+ if Capability.BATCH_FALLBACK in caps and (
83
+ cls.run_tests is Backend.run_tests
84
+ and cls.run_single_test is Backend.run_single_test
85
+ ):
86
+ raise TypeError(
87
+ f"@register('{name}'): {cls.__name__} declares "
88
+ f"Capability.BATCH_FALLBACK but overrides neither "
89
+ f"run_tests() nor run_single_test(). Implement one or "
90
+ f"drop the capability flag."
91
+ )
92
+ if (
93
+ Capability.PERSISTENT_WORKERS in caps
94
+ # review 2026-07-06 (finding 28): classmethods must be compared via
95
+ # __func__ — `cls.persistent_backend_cls is Backend.
96
+ # persistent_backend_cls` compares per-access bound-method wrappers
97
+ # and is always False, which left this check permanently inert.
98
+ and cls.persistent_backend_cls.__func__
99
+ is Backend.persistent_backend_cls.__func__
100
+ ):
101
+ raise TypeError(
102
+ f"@register('{name}'): {cls.__name__} declares "
103
+ f"Capability.PERSISTENT_WORKERS but does not override "
104
+ f"persistent_backend_cls(). Return your persistent-backend "
105
+ f"class (lazy-imported) or drop the capability flag."
106
+ )
107
+ if (
108
+ Capability.LIVE_RESULTS in caps
109
+ and cls.live_result_path.__func__ is Backend.live_result_path.__func__
110
+ ):
111
+ raise TypeError(
112
+ f"@register('{name}'): {cls.__name__} declares "
113
+ f"Capability.LIVE_RESULTS but does not override "
114
+ f"live_result_path(). Return the file that grows during a run "
115
+ f"or drop the capability flag."
116
+ )
117
+
118
+
119
+ def register(name: str):
120
+ """Class decorator that registers a Backend subclass.
121
+
122
+ Validates capability honesty at decoration time — see
123
+ :func:`_validate_capabilities`. A typo or stale flag becomes an
124
+ ``ImportError`` (via ``TypeError``) at module-import time rather
125
+ than a confusing ``NotImplementedError`` mid-run.
126
+ """
127
+
128
+ def decorator(cls: type[Backend]) -> type[Backend]:
129
+ _validate_capabilities(cls, name)
130
+ _REGISTRY[name] = cls
131
+ return cls
132
+
133
+ return decorator
134
+
135
+
136
+ def get_backend(config: Config) -> Backend:
137
+ """Instantiate the backend specified in *config*.
138
+
139
+ Backends self-register via the ``@register`` decorator. Importing the
140
+ backend module triggers registration.
141
+ """
142
+ return get_backend_class(config)(config)
143
+
144
+
145
+ def get_backend_class(config: Config) -> type[Backend]:
146
+ """Return the backend *class* for the config's backend, without instantiating.
147
+
148
+ Useful for reading class-level attributes (e.g. ``artifact_files``) from
149
+ contexts that don't want to pay instantiation cost or raise the
150
+ unavailable-backend ``RuntimeError`` an optional dependency's absence
151
+ signals (FmpyBackend.__init__ imports fmpy).
152
+ """
153
+ backend = config.backend
154
+ if backend not in _REGISTRY:
155
+ _import_builtin_backend(backend)
156
+ cls = _REGISTRY.get(backend)
157
+ if cls is None:
158
+ available = ", ".join(sorted(_REGISTRY)) or "(none)"
159
+ raise ValueError(
160
+ f"Unsupported backend: {backend} "
161
+ f"(from '{config.backend_name}'). Available: {available}"
162
+ )
163
+ return cls
164
+
165
+
166
+ class BackendUnavailableError(RuntimeError):
167
+ """The configured backend cannot be built on this machine.
168
+
169
+ Not a bug: an unregistered backend name, a missing executable, or a
170
+ persistent variant whose dependency probe failed. Callers decide what
171
+ to do about it — the CLI prints the message and exits 1
172
+ (:func:`cli.run_cmds._get_backend`), the authoring tool shows it on the
173
+ job that asked for the run, and ``Session.open`` turns it into a
174
+ ``ConfigError`` so ``dstf author`` refuses the project with one line.
175
+ """
176
+
177
+
178
+ def select_backend(config, *, persistent: bool) -> Backend:
179
+ """Instantiate the backend for *config*, persistent variant when asked.
180
+
181
+ When *persistent* is True and the backend **declares**
182
+ :attr:`Capability.PERSISTENT_WORKERS`, pre-flight the variant's
183
+ dependency probe and instantiate it. When the probe raises
184
+ :class:`RuntimeError`, raise :class:`BackendUnavailableError` naming
185
+ both fixes (install the dep, or re-run with ``--batch``) — no silent
186
+ fallback.
187
+
188
+ The declaration is what gates, not the presence of the hook: a backend
189
+ that ships an experimental ``persistent_backend_cls`` it isn't ready to
190
+ have driven says so by leaving the flag off, and lands on the batch
191
+ path. Registration-time validation (:func:`_validate_capabilities`)
192
+ guarantees the converse — flag implies a real override — so a declaring
193
+ backend always has a class to return.
194
+
195
+ Backend-agnostic: no string matching on backend names. New persistent
196
+ backends plug in by declaring the capability and overriding
197
+ ``persistent_backend_cls`` + ``preflight`` on their backend classes.
198
+
199
+ This function itself reads ``config.backend`` only to phrase the
200
+ persistent-failure message; :func:`get_backend_class`, which it calls
201
+ first, requires the attribute.
202
+ """
203
+ try:
204
+ batch_cls = get_backend_class(config)
205
+ except ValueError as exc:
206
+ # An unregistered backend name is a configuration problem, not a
207
+ # bug; same class of failure as a missing executable.
208
+ raise BackendUnavailableError(str(exc)) from exc
209
+ if not persistent or Capability.PERSISTENT_WORKERS not in batch_cls.capabilities:
210
+ # Batch-only backend (FMPy, Python today). Persistent flag is a
211
+ # no-op rather than an error so a single CLI works across mixed
212
+ # backends in a multi-library config.
213
+ return _instantiate(batch_cls, config)
214
+ pcls = batch_cls.persistent_backend_cls()
215
+ if pcls is None:
216
+ # Unreachable for a validated backend (the flag implies the
217
+ # override); kept as the type narrowing for the call below.
218
+ return _instantiate(batch_cls, config)
219
+ try:
220
+ pcls.preflight(config)
221
+ except RuntimeError as exc:
222
+ raise BackendUnavailableError(
223
+ f"Persistent-worker {config.backend} unavailable.\n"
224
+ f"\n"
225
+ f"{exc}\n"
226
+ f"\n"
227
+ f"Or re-run with `--batch` to use the per-test subprocess "
228
+ f"backend instead."
229
+ ) from exc
230
+ return _instantiate(pcls, config)
231
+
232
+
233
+ def _instantiate(cls: type[Backend], config) -> Backend:
234
+ """``cls(config)``, with a missing-executable ``RuntimeError`` translated.
235
+
236
+ The ``try`` wraps exactly the constructor call: backends raise
237
+ ``RuntimeError`` when the tool they drive isn't installed
238
+ (``python/backend.py``, ``julia/backend.py``), which is an unavailable
239
+ backend, while any other exception is a genuine bug and keeps its
240
+ traceback.
241
+ """
242
+ try:
243
+ return cls(config)
244
+ except RuntimeError as exc:
245
+ raise BackendUnavailableError(str(exc)) from exc
246
+
247
+
248
+ def _import_builtin_backend(name: str) -> None:
249
+ """Import a built-in backend module so it registers itself."""
250
+ builtins = {
251
+ "Dymola": ".dymola",
252
+ "FMPy": ".fmpy",
253
+ "OpenModelica": ".openmodelica",
254
+ "Julia": ".julia",
255
+ "Python": ".python",
256
+ }
257
+ module = builtins.get(name)
258
+ if module:
259
+ import importlib
260
+
261
+ importlib.import_module(module, package=__name__)
262
+
263
+
264
+ def dataset_kind_for(backend):
265
+ """The single :class:`DatasetType` *backend* declares, else the default.
266
+
267
+ ``produced_datasets`` is the backend's declaration of what shape its
268
+ results carry; the MetricTree evaluator refuses a leaf whose metric
269
+ can't score that shape. When a backend declares exactly one kind we can
270
+ say authoritatively which it is, so we thread it. A backend declaring
271
+ several can't be resolved from the class alone — the kind would have to
272
+ come off each result — so we fall back to ``TIME_SERIES``, which is what
273
+ the evaluator assumes anyway. All five builtins declare exactly
274
+ ``{TIME_SERIES}``, so this is behaviour-neutral today; it exists so the
275
+ first backend to declare something else is scored against the right
276
+ metric set without a second wiring pass.
277
+ """
278
+ from .types import DatasetType
279
+
280
+ kinds = type(backend).produced_datasets
281
+ if len(kinds) == 1:
282
+ return next(iter(kinds))
283
+ return DatasetType.TIME_SERIES