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,249 @@
1
+ """A plot's event hook (two-tier design §4.6 "Panel", amended in sub-project 6
2
+ and in 3b; D131): ``panel.figure.on(kind, handler)``. One hook per plot card,
3
+ created with the card and attached to its ``ui.plotly`` when the plot is
4
+ drawn; a panel hosted under that card subscribes through ``owned(owner)``, so
5
+ closing or rebuilding it takes back exactly its handlers."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from nicegui import ui
14
+
15
+ # The index is bounded so a hostile key cannot reach `int()`'s digit limit;
16
+ # `[0-9]`, not `\d` (which matches any Unicode digit, and `int()` accepts
17
+ # those), and `\Z`, not `$` (which also matches before a trailing newline).
18
+ _SHAPE_KEY = re.compile(r"^shapes\[([0-9]{1,9})\]\.(.+)\Z")
19
+
20
+
21
+ class FigureHook:
22
+ """``panel.figure.on(kind, handler)``: handlers get a primitive id and a
23
+ payload. Wired to one plot's Plotly events — the card's, so a drag on one
24
+ plot of a stacked page reaches that card's subscribers and no other. The
25
+ payload contract (the spike's, over the payloads plotly.js actually
26
+ sends):
27
+
28
+ - ``plotly_click`` / ``plotly_selected``: the id of the first point whose
29
+ trace ``meta.kind`` is ``"handle"``, else of the first point —
30
+ ``customdata``, then ``meta.id``, then ``curveNumber`` — with the event
31
+ payload. Under the figure's ``hovermode: "x unified"`` a click lists
32
+ every trace at that x, so a handle is not necessarily ``points[0]``.
33
+ - ``plotly_relayout`` with keys matching ``shapes[N].<key>``: dispatched
34
+ ONCE PER SHAPE INDEX, ascending. The payload never names the shape;
35
+ the hook resolves the id from the rendered figure's
36
+ ``layout.shapes[N].name`` (``""`` when that lookup fails) and hands the
37
+ handler that shape's keys with the ``shapes[N].`` prefix stripped —
38
+ ``{"y0": 6.0, "y1": 6.0}`` for a line, ``{"xanchor": 4.0, "yanchor":
39
+ 2.99, "path": "M …"}`` for a ring (a pixel path). Positions arrive in
40
+ data coordinates. The hook does not interpret them: the capability does,
41
+ from the handle's primitive type (a Marker's ring moves only by
42
+ ``xanchor``/``yanchor``). ``on_shape_moved`` is then called once, in a
43
+ ``finally``: a raising handler still gets its snap-back. A relayout
44
+ mixing shape keys with others (a zoom) is a shape relayout; the other
45
+ keys are dropped.
46
+ - Any other relayout (a zoom): dispatched once with id ``""`` and the raw
47
+ payload (``{}`` when it is not a dict); ``on_shape_moved`` is not called.
48
+ - ``dstf_move`` (D149; `move`): a handle HELD on the plot. The page
49
+ sends ``{"seq": n, "update": {...}}``, ``update`` shaped as the
50
+ relayout its release would be, and it is dispatched as one — once per
51
+ shape index, the id resolved the same way — but ``on_shape_moved`` is
52
+ not called: nothing moved. ``{"seq": n, "end": True}`` (a drag that
53
+ ended with no release) is dispatched once with id ``""``. Every move
54
+ and every release carries a sequence number, the page's, rising: a
55
+ move numbered at or under the last release or end heard is DROPPED —
56
+ a preview that arrives after its drag was let go must not paint over
57
+ what the release staged.
58
+ - ``dstf_click`` / anything else a caller hands `emit`: dispatched once
59
+ with id ``""`` and the payload as given.
60
+
61
+ Handler semantics: each shape index is dispatched in turn, ascending,
62
+ from its own snapshot of the handlers, taken when that shape's dispatch
63
+ starts. A handler registered since the snapshot (a panel rebuilt by the
64
+ act it triggered) is not called for that shape; one removed since the
65
+ snapshot (``clear``) is skipped, checked by identity against the current
66
+ list, so a rebuilt panel's predecessor never runs. A handler registered
67
+ twice is called twice. A handler that raises ENDS the relayout: later
68
+ handlers and later shapes are not dispatched, the exception propagates,
69
+ and ``on_shape_moved`` still runs. A malformed payload never makes
70
+ dispatch raise."""
71
+
72
+ def __init__(self, on_shape_moved: Callable[[], None] | None = None) -> None:
73
+ self._on_shape_moved = on_shape_moved
74
+ self._handlers: dict[str, list[tuple[str, Callable[[str, dict], None]]]] = {}
75
+ self._plot: Any = None
76
+ self.dispatching = False # true while handlers run
77
+ #: The sequence number of the last release or drag end heard: a
78
+ #: `move` numbered at or under it is of a drag already over.
79
+ self.released: float = -1
80
+
81
+ def clear(self, owner: str | None = None) -> None:
82
+ """Remove one owner's handlers, or every handler. A host rebuilds its
83
+ panels whenever what they show can have moved, and the card itself is
84
+ dropped on a rebuild of the page; a subscriber that re-registers in
85
+ `panel()` must not accumulate."""
86
+ if owner is None:
87
+ self._handlers = {}
88
+ return
89
+ # `_call` skips a handler removed mid-dispatch by IDENTITY against
90
+ # the current list, so the entries left here must be the SAME tuple
91
+ # objects — rebuilding them (`(o, h)` anew) would make every
92
+ # surviving handler look removed and skip it.
93
+ self._handlers = {
94
+ kind: [entry for entry in entries if entry[0] != owner]
95
+ for kind, entries in self._handlers.items()
96
+ }
97
+
98
+ def on(
99
+ self, kind: str, handler: Callable[[str, dict], None], owner: str = ""
100
+ ) -> None:
101
+ self._handlers.setdefault(kind, []).append((owner, handler))
102
+
103
+ def owned(self, owner: str) -> _OwnedFigureHook:
104
+ """The view of this hook a capability's panel holds: its `on`
105
+ registers under ``owner``, so `clear(owner)` removes exactly those."""
106
+ return _OwnedFigureHook(self, owner)
107
+
108
+ def attach(self, plot: ui.plotly, relayout_js: str | None = None) -> None:
109
+ """Wire ``plot``'s events to this hook. ``relayout_js``: a
110
+ JavaScript expression over ``e``, the relayout's payload in the
111
+ page, whose value is sent in its place — what the page alone can
112
+ say of a release (the card holds a dragged grip to the VISIBLE
113
+ range, which no server knows). The hook still interprets
114
+ nothing."""
115
+ self._plot = plot
116
+ for event in ("plotly_click", "plotly_relayout", "plotly_selected"):
117
+
118
+ def heard(e: Any, kind: str = event) -> None:
119
+ self._dispatch(kind, e.args)
120
+
121
+ if event == "plotly_relayout" and relayout_js is not None:
122
+ plot.on(event, heard, js_handler=f"(e) => emit({relayout_js})")
123
+ else:
124
+ plot.on(event, heard)
125
+
126
+ def emit(self, kind: str, payload: dict, primitive: str = "") -> None:
127
+ """One event of ``kind`` to its subscribers, as a dispatch: what
128
+ the card hears from the page beside Plotly's own events (a click),
129
+ and what one panel says to the others (a preview)."""
130
+ self._call(kind, primitive, payload if isinstance(payload, dict) else {})
131
+
132
+ def move(self, payload: Any) -> None:
133
+ """A held handle's would-be position, from the page (`dstf_move`
134
+ above). Dropped where its drag is already over."""
135
+ data = payload if isinstance(payload, dict) else {}
136
+ seq = data.get("seq")
137
+ if not isinstance(seq, (int, float)) or isinstance(seq, bool):
138
+ return
139
+ if seq <= self.released:
140
+ return
141
+ if data.get("end"):
142
+ self.released = seq
143
+ self._call("dstf_move", "", {"end": True})
144
+ return
145
+ for i, shape in sorted(_by_shape(data.get("update")).items()):
146
+ self._call("dstf_move", self._shape_name(i), shape)
147
+
148
+ def _shape_name(self, index: int) -> str:
149
+ # A known race: the index is resolved against the figure the SERVER
150
+ # last sent. A drag released after an in-place `update_figure` that
151
+ # changed the shape list, but resolved before the browser has applied
152
+ # that update and the relayout has flown back, can resolve to the
153
+ # wrong name. The window runs from `update_figure` through both
154
+ # websocket legs. The snap-back redraw corrects only the picture: once
155
+ # edits are staged from drags, a wrongly resolved handle's edit stays
156
+ # staged.
157
+ figure = getattr(self._plot, "figure", None)
158
+ layout = figure.get("layout") if isinstance(figure, dict) else None
159
+ shapes = layout.get("shapes") if isinstance(layout, dict) else None
160
+ if not isinstance(shapes, list) or index >= len(shapes):
161
+ return ""
162
+ shape = shapes[index]
163
+ name = shape.get("name") if isinstance(shape, dict) else None
164
+ return name if isinstance(name, str) else ""
165
+
166
+ def _dispatch(self, kind: str, payload: Any) -> None:
167
+ data = payload if isinstance(payload, dict) else {}
168
+ if kind == "plotly_relayout":
169
+ by_shape = _by_shape(data)
170
+ if not by_shape:
171
+ self._call(kind, "", data)
172
+ return
173
+ # The release's number, before any handler runs: whatever
174
+ # move of this drag is still on its way is late from here on.
175
+ for shape in by_shape.values():
176
+ if _is_seq(shape.get("seq")):
177
+ self.released = max(self.released, shape["seq"])
178
+ try:
179
+ for i in sorted(by_shape):
180
+ self._call(kind, self._shape_name(i), by_shape[i])
181
+ finally:
182
+ if self._on_shape_moved is not None:
183
+ self._on_shape_moved()
184
+ return
185
+ points = data.get("points")
186
+ points = (
187
+ [p for p in points if isinstance(p, dict)]
188
+ if isinstance(points, list)
189
+ else []
190
+ )
191
+ chosen = next(
192
+ (p for p in points if _meta(p).get("kind") == "handle"),
193
+ points[0] if points else {},
194
+ )
195
+ # The figure layer stamps every trace's primitive id in `meta.id`
196
+ # (figures/plotly.py); marker groups carry per-point ids in
197
+ # `customdata`. The trace index is the last resort.
198
+ primitive = chosen.get("customdata")
199
+ if primitive is None:
200
+ primitive = _meta(chosen).get("id")
201
+ if primitive is None:
202
+ primitive = chosen.get("curveNumber")
203
+ self._call(kind, "" if primitive is None else str(primitive), data)
204
+
205
+ def _call(self, kind: str, primitive: str, payload: dict) -> None:
206
+ """One event: the snapshot is taken here, and each entry is checked
207
+ by identity against the current list just before it is called."""
208
+ snapshot = list(self._handlers.get(kind, []))
209
+ was, self.dispatching = self.dispatching, True
210
+ try:
211
+ for entry in snapshot:
212
+ if not any(e is entry for e in self._handlers.get(kind, [])):
213
+ continue # removed since the snapshot
214
+ entry[1](primitive, payload)
215
+ finally:
216
+ self.dispatching = was
217
+
218
+
219
+ def _is_seq(value: object) -> bool:
220
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
221
+
222
+
223
+ def _by_shape(update: object) -> dict[int, dict]:
224
+ """A relayout's ``shapes[N].<key>`` entries, grouped by shape index
225
+ with the prefix stripped; every other key is dropped."""
226
+ by_shape: dict[int, dict] = {}
227
+ if not isinstance(update, dict):
228
+ return by_shape
229
+ for key, value in update.items():
230
+ match = _SHAPE_KEY.match(key) if isinstance(key, str) else None
231
+ if match is not None:
232
+ by_shape.setdefault(int(match.group(1)), {})[match.group(2)] = value
233
+ return by_shape
234
+
235
+
236
+ def _meta(point: dict) -> dict:
237
+ trace = point.get("data")
238
+ meta = trace.get("meta") if isinstance(trace, dict) else None
239
+ return meta if isinstance(meta, dict) else {}
240
+
241
+
242
+ class _OwnedFigureHook:
243
+ """`FigureHook.owned(owner)`: satisfies the capability-side protocol."""
244
+
245
+ def __init__(self, hook: FigureHook, owner: str) -> None:
246
+ self._hook, self._owner = hook, owner
247
+
248
+ def on(self, kind: str, handler: Callable[[str, dict], None]) -> None:
249
+ self._hook.on(kind, handler, owner=self._owner)