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,248 @@
1
+ """What a project's source files looked like at one moment.
2
+
3
+ A simulator that stays alive (the tool's queue: Dymola, OpenModelica, Julia)
4
+ read the library once, when its worker started; a file edited since is not
5
+ what it runs. A :class:`LibraryState` is the stamp — modification time and
6
+ size — of every source file at one Queue click, so "has anything changed
7
+ since this worker read the library" is a comparison of two states and, when
8
+ the answer is no, costs nothing in the simulator.
9
+
10
+ Which files, by source type (:func:`source_stamps`): a Modelica library's
11
+ ``*.mo`` and ``package.order`` under ``source_path`` and under every
12
+ dependency given as a PATH; a Julia project's ``*.jl`` and ``*.toml``. A
13
+ dependency given by NAME (``"Modelica"``) is found by the simulator, not by
14
+ this tool, and is not swept. An FMU or Python project runs one process per
15
+ test, which reads its source as it is: nothing is swept.
16
+
17
+ Stat only, no file is opened. Over a 9P mount (``/mnt/d``) LISTING a
18
+ directory is the slow step — a library of 4 117 such files in 872
19
+ directories took 6.9 s one directory after the other — so the walk fans
20
+ the directories out over threads there (1.3 s with 8; 16 are no faster). On
21
+ a native file system one thread walks the same library in 0.06 s and the
22
+ threads would only cost (0.57 s), so a root is walked on threads only when
23
+ its own listing was slow.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import time
30
+ from collections.abc import Callable, Iterable, Mapping
31
+ from concurrent.futures import ThreadPoolExecutor
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+ from typing import TYPE_CHECKING
35
+
36
+ if TYPE_CHECKING:
37
+ from ..config import Config
38
+
39
+ #: ``(st_mtime_ns, st_size)``.
40
+ Stamp = tuple[int, int]
41
+
42
+ #: A root whose own listing took longer than this is walked on threads.
43
+ _SLOW_LISTING_SECONDS = 0.002
44
+ _THREADS = 8
45
+
46
+
47
+ def stamp(path: Path | str) -> Stamp | None:
48
+ """The stamp of one file now; None when there is no such file."""
49
+ try:
50
+ st = os.stat(path)
51
+ except OSError:
52
+ return None
53
+ return st.st_mtime_ns, st.st_size
54
+
55
+
56
+ def _list(
57
+ directory: str, wanted: Callable[[str], bool]
58
+ ) -> tuple[list[str], list[str], dict[Path, Stamp]]:
59
+ """One directory: its subdirectories, those of them that are symbolic
60
+ links (apart: :func:`_followed` decides which are walked), and the
61
+ stamp of each wanted file in it. A link is read as what it points at;
62
+ a dangling one is nothing."""
63
+ subs: list[str] = []
64
+ links: list[str] = []
65
+ found: dict[Path, Stamp] = {}
66
+ try:
67
+ entries = os.scandir(directory)
68
+ except OSError: # removed while the walk ran, or not a directory
69
+ return subs, links, found
70
+ with entries:
71
+ for entry in entries:
72
+ try:
73
+ if entry.is_dir():
74
+ (links if entry.is_symlink() else subs).append(entry.path)
75
+ elif wanted(entry.name):
76
+ st = entry.stat()
77
+ found[Path(entry.path)] = (st.st_mtime_ns, st.st_size)
78
+ except OSError:
79
+ continue
80
+ return subs, links, found
81
+
82
+
83
+ def _followed(links: list[str], seen: set[str]) -> list[str]:
84
+ """The linked directories of *links* to walk: each real directory is
85
+ entered through a link once, and the root never — so a link that leads
86
+ back to a directory above it ends the walk instead of repeating it. In
87
+ name order, so two links to one directory are told apart the same way
88
+ by every sweep."""
89
+ walked: list[str] = []
90
+ for link in sorted(links):
91
+ real = os.path.realpath(link)
92
+ if real not in seen:
93
+ seen.add(real)
94
+ walked.append(link)
95
+ return walked
96
+
97
+
98
+ def sweep(roots: Iterable[Path], wanted: Callable[[str], bool]) -> dict[Path, Stamp]:
99
+ """The stamp of every file under *roots* whose name is *wanted*, keyed
100
+ by its path as it is spelled from its root — through a sub-directory
101
+ that is a symbolic link too (a sub-package kept elsewhere is part of
102
+ the library the simulator loads). A root that is a file is stamped
103
+ itself; one that does not exist holds nothing."""
104
+ stamps: dict[Path, Stamp] = {}
105
+ for root in roots:
106
+ if not root.is_dir():
107
+ own = stamp(root)
108
+ if own is not None:
109
+ stamps[root] = own
110
+ continue
111
+ seen = {os.path.realpath(root)}
112
+ began = time.perf_counter()
113
+ pending, links, found = _list(str(root), wanted)
114
+ pending += _followed(links, seen)
115
+ stamps.update(found)
116
+ if time.perf_counter() - began <= _SLOW_LISTING_SECONDS:
117
+ while pending:
118
+ subs, links, found = _list(pending.pop(), wanted)
119
+ pending += subs + _followed(links, seen)
120
+ stamps.update(found)
121
+ continue
122
+ with ThreadPoolExecutor(_THREADS, thread_name_prefix="dstf-sweep") as pool:
123
+ while pending:
124
+ listed = list(pool.map(lambda sub: _list(sub, wanted), pending))
125
+ pending = [sub for subs, _, _ in listed for sub in subs]
126
+ pending += _followed(
127
+ [link for _, links, _ in listed for link in links], seen
128
+ )
129
+ for _, _, found in listed:
130
+ stamps.update(found)
131
+ return stamps
132
+
133
+
134
+ def _modelica_source(name: str) -> bool:
135
+ return name.endswith(".mo") or name == "package.order"
136
+
137
+
138
+ def _julia_source(name: str) -> bool:
139
+ return name.endswith((".jl", ".toml"))
140
+
141
+
142
+ def dependency_root(entry: str) -> Path | None:
143
+ """Where a ``dependencies`` entry's files are: the directory of a
144
+ library given by its directory or its ``package.mo``, the one file of a
145
+ library given as ``Name.mo``; None for a library given by name. Spelled
146
+ as the entry is, and the ONE place that says it: the sweep keys a
147
+ dependency's files under this, and a backend asks :func:`under` with
148
+ this which of them a change touched."""
149
+ from ..config import dependency_is_path
150
+
151
+ if not dependency_is_path(entry):
152
+ return None
153
+ path = Path(entry)
154
+ return path.parent if path.name == "package.mo" else path
155
+
156
+
157
+ def dependency_file(entry: str) -> Path:
158
+ """The file a ``dependencies`` entry given as a path is LOADED by,
159
+ resolved: the entry itself when it is a ``.mo`` file — a one-file
160
+ library ``Name.mo``, or a library's own ``package.mo`` — else the
161
+ ``package.mo`` of its directory. The ONE place that says it, for every
162
+ simulator's start, reload and batch script: Dymola was handed
163
+ ``<entry>/package.mo`` whatever the entry, and a one-file dependency
164
+ (``…/DepLib.mo/package.mo``) started no worker."""
165
+ path = Path(entry).resolve()
166
+ return path if path.suffix == ".mo" else path / "package.mo"
167
+
168
+
169
+ def under(path: Path, root: Path) -> bool:
170
+ """Whether a swept *path* is *root* or a file below it — both as the
171
+ sweep spells them: *root* is the library's directory or a
172
+ :func:`dependency_root`, never a resolved form of either (a path
173
+ through a symbolic link, or one holding ``..``, has another)."""
174
+ return path == root or root in path.parents
175
+
176
+
177
+ def source_stamps(config: Config) -> dict[Path, Stamp] | None:
178
+ """The stamp of every source file a live simulator holds of *config*'s
179
+ project; None for a source type no worker keeps loaded."""
180
+ if config.source_type == "modelica":
181
+ roots = [config.library_dir]
182
+ roots += [
183
+ root
184
+ for entry in config.dependencies
185
+ if (root := dependency_root(entry)) is not None
186
+ ]
187
+ return sweep(roots, _modelica_source)
188
+ if config.source_type == "julia" and config.source_path is not None:
189
+ return sweep([config.source_path], _julia_source)
190
+ return None
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class LibraryChanges:
195
+ """The files that differ between two states, each list sorted."""
196
+
197
+ #: In both, with another stamp.
198
+ changed: tuple[Path, ...] = ()
199
+ added: tuple[Path, ...] = ()
200
+ removed: tuple[Path, ...] = ()
201
+
202
+ def __bool__(self) -> bool:
203
+ return bool(self.changed or self.added or self.removed)
204
+
205
+
206
+ @dataclass(frozen=True, eq=False)
207
+ class LibraryState:
208
+ """The source files at one sweep. ``serial`` orders the states of one
209
+ Session: a higher one was swept later. Two states are the same state
210
+ only when they are the same object (:func:`following`)."""
211
+
212
+ serial: int
213
+ stamps: Mapping[Path, Stamp]
214
+
215
+ def changes_since(self, older: LibraryState) -> LibraryChanges:
216
+ new, old = self.stamps, older.stamps
217
+ return LibraryChanges(
218
+ changed=tuple(
219
+ sorted(p for p, s in new.items() if p in old and old[p] != s)
220
+ ),
221
+ added=tuple(sorted(p for p in new if p not in old)),
222
+ removed=tuple(sorted(p for p in old if p not in new)),
223
+ )
224
+
225
+
226
+ def following(last: LibraryState | None, stamps: Mapping[Path, Stamp]) -> LibraryState:
227
+ """The state a sweep that found *stamps* is: *last* itself when no file
228
+ differs from it — whoever holds *last* then has nothing to read — else
229
+ the next one."""
230
+ if last is None:
231
+ return LibraryState(1, stamps)
232
+ if stamps == last.stamps:
233
+ return last
234
+ return LibraryState(last.serial + 1, stamps)
235
+
236
+
237
+ __all__ = [
238
+ "LibraryChanges",
239
+ "LibraryState",
240
+ "Stamp",
241
+ "dependency_file",
242
+ "dependency_root",
243
+ "following",
244
+ "source_stamps",
245
+ "stamp",
246
+ "sweep",
247
+ "under",
248
+ ]