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
|
@@ -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
|
+
]
|