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
dstf/plugins.py ADDED
@@ -0,0 +1,104 @@
1
+ """Entry-point plug-in loading — the seam third-party packages register through.
2
+
3
+ Every extension registry in the framework (``backends.register``,
4
+ ``discovery.recognizer.register``, ``comparison.registry.register_metric`` /
5
+ ``register_combinator``) is populated by *importing* the module that calls it.
6
+ Builtins are imported by their package ``__init__``; a third-party plug-in
7
+ lives in a package we never import, so it needs a hook. That hook is a single
8
+ entry-point group:
9
+
10
+ .. code-block:: toml
11
+
12
+ # in the plug-in package's pyproject.toml
13
+ [project.entry-points."dstf.plugins"]
14
+ acme_backend = "acme_dstf.backend" # module: import side effects register
15
+ acme_metrics = "acme_dstf.metrics:register" # callable: invoked with no args
16
+
17
+ One group, not one per layer: a plug-in package routinely ships a backend
18
+ *and* the metrics that score it, and splitting the declaration by layer only
19
+ makes the author guess which group a given registration belongs to.
20
+
21
+ A plug-in that fails to import or call is a **hard error**
22
+ (:class:`~dstf.config.ConfigError`). A metric that silently failed to register
23
+ does not disappear quietly — it changes scoring semantics, either by failing
24
+ spec parse with a confusing "unknown metric" or by falling back to a default.
25
+ Fail at startup, naming the entry point.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import importlib.metadata
31
+ from types import ModuleType
32
+
33
+ #: The single entry-point group third-party packages advertise plug-ins under.
34
+ PLUGIN_GROUP = "dstf.plugins"
35
+
36
+ # None until the first successful load. Loading is idempotent: the CLI calls
37
+ # it once per process, but a library user may call it again from anywhere.
38
+ _loaded: list[str] | None = None
39
+ _completed: set[str] = set()
40
+
41
+
42
+ def load_plugins() -> list[str]:
43
+ """Import every ``dstf.plugins`` entry point, returning the names loaded.
44
+
45
+ Each entry point's ``.load()`` result is either a **module** (the import
46
+ itself performed the registrations) or a **callable** (invoked with no
47
+ arguments). Anything else is an error.
48
+
49
+ Idempotent — the second and later calls return the same names without
50
+ re-importing. Raises :class:`~dstf.config.ConfigError` naming the failing
51
+ entry point, chaining the original exception.
52
+ """
53
+ global _loaded
54
+ if _loaded is not None:
55
+ return list(_loaded)
56
+
57
+ # Per-entry-point completion tracking, not one all-or-nothing flag: a
58
+ # library user who catches the ConfigError from a broken plug-in and
59
+ # retries must not re-invoke the HEALTHY plug-ins that already ran —
60
+ # their re-registration would raise the registry's duplicate error and
61
+ # blame the wrong plug-in (B2 review finding 4).
62
+ names: list[str] = []
63
+ for ep in importlib.metadata.entry_points(group=PLUGIN_GROUP):
64
+ if ep.name in _completed:
65
+ names.append(ep.name)
66
+ continue
67
+ try:
68
+ obj = ep.load()
69
+ if isinstance(obj, ModuleType):
70
+ pass # import side effects did the registration
71
+ elif isinstance(obj, type):
72
+ # A class is callable, but instantiating it with no args is
73
+ # never what a plug-in author meant (B2 review finding 8).
74
+ raise TypeError(
75
+ f"resolved to class {obj.__name__}; point the entry "
76
+ f"point at a module or a no-argument callable"
77
+ )
78
+ elif callable(obj):
79
+ obj()
80
+ else:
81
+ raise TypeError(
82
+ f"resolved to {type(obj).__name__}, expected a module or a callable"
83
+ )
84
+ except Exception as exc:
85
+ # Lazy — config imports enough of the package that a module-level
86
+ # import here would risk a cycle, and this is the cold path.
87
+ from .config import ConfigError
88
+
89
+ raise ConfigError(
90
+ f"Failed to load DSTF plug-in '{ep.name}' "
91
+ f"({PLUGIN_GROUP} = {ep.value!r}): {exc}"
92
+ ) from exc
93
+ _completed.add(ep.name)
94
+ names.append(ep.name)
95
+
96
+ _loaded = names
97
+ return list(names)
98
+
99
+
100
+ def _reset_for_tests() -> None:
101
+ """Forget that plug-ins were loaded — unit tests only."""
102
+ global _loaded
103
+ _completed.clear()
104
+ _loaded = None
File without changes
@@ -0,0 +1,187 @@
1
+ """Console (terminal) report for test comparison results."""
2
+
3
+ import sys
4
+
5
+ from ..comparison.comparator import TestComparison
6
+ from .status import (
7
+ FAIL,
8
+ NO_REF,
9
+ NOT_RUN,
10
+ SIM_FAIL,
11
+ STATUS_LABELS,
12
+ FailedCriterion,
13
+ advisory_failures,
14
+ advisory_text,
15
+ classify,
16
+ exit_code,
17
+ failed_criteria,
18
+ group_failures,
19
+ )
20
+
21
+ # ANSI color codes
22
+ _GREEN = "\033[92m"
23
+ _RED = "\033[91m"
24
+ _YELLOW = "\033[93m"
25
+ _GREY = "\033[90m"
26
+ _RESET = "\033[0m"
27
+ _BOLD = "\033[1m"
28
+
29
+
30
+ def _color(text: str, code: str) -> str:
31
+ """Wrap text in ANSI color if stdout is a terminal."""
32
+ if sys.stdout.isatty():
33
+ return f"{code}{text}{_RESET}"
34
+ return text
35
+
36
+
37
+ def print_report(comparisons: list[TestComparison]) -> int:
38
+ """Print a console report and return exit code (0 when nothing FAILED or
39
+ failed to simulate — NOT RUN and NO_REF do not fail it — and something
40
+ ran, else 1: ``exit_code``)."""
41
+ if not comparisons:
42
+ print("No comparisons to report.")
43
+ return 1
44
+
45
+ n_passed = 0
46
+ n_failed = 0
47
+ n_no_ref = 0
48
+ n_sim_fail = 0
49
+ n_not_run = 0
50
+ failures: list[TestComparison] = []
51
+ warned: list[TestComparison] = []
52
+ advised: list[tuple[TestComparison, list[FailedCriterion]]] = []
53
+
54
+ for comp in comparisons:
55
+ # Ordering (SIM_FAIL > FAIL > NO_REF > PASS) and its rationale live
56
+ # in reporting.status — this is one of its call sites, not a copy.
57
+ state = classify(comp)
58
+ if state == NOT_RUN:
59
+ # No result: nothing ran, so nothing failed (D145).
60
+ n_not_run += 1
61
+ status = _color(STATUS_LABELS[NOT_RUN], _GREY)
62
+ elif state == SIM_FAIL:
63
+ n_sim_fail += 1
64
+ status = _color("SIM_FAIL", _RED)
65
+ elif state == FAIL:
66
+ n_failed += 1
67
+ status = _color("FAIL", _RED)
68
+ elif state == NO_REF:
69
+ n_no_ref += 1
70
+ status = _color("NO_REF", _YELLOW)
71
+ else:
72
+ n_passed += 1
73
+ status = _color("PASS", _GREEN)
74
+
75
+ # Show warning indicator if structural changes detected
76
+ warn_tag = ""
77
+ if comp.warnings:
78
+ warn_tag = _color(" [WARN]", _YELLOW)
79
+ warned.append(comp)
80
+
81
+ if comp.test_id:
82
+ id_tag = f"[{comp.test_id}] "
83
+ elif not comp.has_reference:
84
+ id_tag = "[new] "
85
+ else:
86
+ id_tag = ""
87
+ # A test that passes with a failure under `warn` says so: it read a
88
+ # plain PASS, with no word of the criterion that failed.
89
+ advisory = advisory_failures(comp)
90
+ advisory_tag = ""
91
+ if advisory:
92
+ advised.append((comp, advisory))
93
+ advisory_tag = _color(f" ({advisory_text(len(advisory))})", _YELLOW)
94
+ print(f" {status} {id_tag}{comp.model_id}{advisory_tag}{warn_tag}")
95
+
96
+ if not comp.passed and comp.sim_success:
97
+ failures.append(comp)
98
+
99
+ # Summary
100
+ total = len(comparisons)
101
+ print()
102
+ print(_color(f"Results: {n_passed}/{total} passed", _BOLD))
103
+ if n_failed:
104
+ print(_color(f" Failed: {n_failed}", _RED))
105
+ if n_sim_fail:
106
+ print(_color(f" Simulation errors: {n_sim_fail}", _RED))
107
+ if n_no_ref:
108
+ print(_color(f" No baseline: {n_no_ref}", _YELLOW))
109
+ if n_not_run:
110
+ print(_color(f" {STATUS_LABELS[NOT_RUN]}: {n_not_run}", _GREY))
111
+ if advised:
112
+ print(_color(f" With advisory failures: {len(advised)}", _YELLOW))
113
+ if warned:
114
+ print(_color(f" Structural warnings: {len(warned)}", _YELLOW))
115
+
116
+ # Print failure details
117
+ if failures:
118
+ print()
119
+ print(_color("Failure Details:", _BOLD))
120
+ print("-" * 80)
121
+ for comp in failures:
122
+ id_tag = f"[{comp.test_id}] " if comp.test_id else ""
123
+ print(f"\n {id_tag}{comp.model_id}")
124
+ if comp.error_message:
125
+ print(f" Error: {comp.error_message}")
126
+ for block in failure_blocks(comp):
127
+ for line in block:
128
+ print(line)
129
+ # A group that fails on its own: the test's reason where no
130
+ # criterion failed.
131
+ for line in group_failures(comp):
132
+ print(f" {line}")
133
+
134
+ # The criteria that failed under a `warn` in the tests that passed
135
+ if advised:
136
+ print()
137
+ print(_color("Advisory Failures (under `warn`: they fail no test):", _BOLD))
138
+ print("-" * 80)
139
+ for comp, advisory in advised:
140
+ id_tag = f"[{comp.test_id}] " if comp.test_id else ""
141
+ print(f"\n {id_tag}{comp.model_id}")
142
+ for block in _blocks(advisory):
143
+ for line in block:
144
+ print(line)
145
+
146
+ # Print structural warnings
147
+ if warned:
148
+ print()
149
+ print(_color("Structural Warnings:", _BOLD))
150
+ print("-" * 80)
151
+ for comp in warned:
152
+ print(f"\n {comp.model_id}")
153
+ for w in comp.warnings:
154
+ print(f" {w.text}")
155
+
156
+ return exit_code(comparisons)
157
+
158
+
159
+ def failure_blocks(comp: TestComparison) -> list[list[str]]:
160
+ """One block of lines per criterion of ``comp`` that did not pass
161
+ (:func:`~.status.failed_criteria`, which JUnit's lines are read from
162
+ too), in TREE order: the line that says which criterion it is, the
163
+ criterion in its metric's own words — or why it was not scored — and
164
+ that metric's detail pairs.
165
+
166
+ The blocks used to be the same five generic numbers under the
167
+ variable's name alone: a start-up window's nrmse and an advisory
168
+ comparison against a soft check read as one variable failing twice,
169
+ with nothing to tell them apart."""
170
+ return _blocks(failed_criteria(comp))
171
+
172
+
173
+ def _blocks(criteria: list[FailedCriterion]) -> list[list[str]]:
174
+ blocks: list[list[str]] = []
175
+ for criterion in criteria:
176
+ lines = [f" {criterion.head}"]
177
+ if criterion.reason:
178
+ # A sentinel (config that could not build, no data to compare)
179
+ # has no meaningful numbers — the reason is the whole finding.
180
+ lines.append(f" {'Not scored:':<20}{criterion.reason}")
181
+ else:
182
+ lines.append(f" {criterion.sentence}")
183
+ lines += [
184
+ f" {label + ':':<20}{value}" for label, value in criterion.details
185
+ ]
186
+ blocks.append(lines)
187
+ return blocks