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,103 @@
1
+ """Pluggable test-recognition system.
2
+
3
+ A ``Recognizer`` inspects a source file (a Modelica ``.mo``, an FMU, a Julia
4
+ script, ...) and emits a ``RecognizerResult`` describing a test it found.
5
+ Discovery runs every registered recognizer that applies to the configured
6
+ ``source_type`` and merges results by ``model_id``. This lets users layer
7
+ their own test-tagging conventions on top of (or in place of) the bundled
8
+ defaults — a library that can't adopt the bundled ``UnitTests`` component
9
+ provides its own recognizer (PTA.2 onward: via a JSON map) instead.
10
+
11
+ PTA.1 wires the registry; only the bundled recognizer registers today, so
12
+ behavior is identical to the pre-PTA hardcoded path.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+
22
+ @dataclass
23
+ class RecognizerResult:
24
+ """One recognizer's contribution toward a ``TestModel`` for one source file.
25
+
26
+ Fields left ``None`` / empty are not contributed by this recognizer; the
27
+ framework defaults fill them in (or another recognizer overrides, when
28
+ multiple recognizers emit for the same ``model_id``).
29
+
30
+ The shape today mirrors what the bundled Modelica recognizer extracts.
31
+ PTA.4 broadens this with runtime-behavior fields (``simulate_only``,
32
+ ``requested_baselines``, ...).
33
+ """
34
+
35
+ model_id: str
36
+ source_file: Path | None = None
37
+ # Variables-to-track contribution (was UnitTestInfo)
38
+ n_vars: int | None = None
39
+ # Instance name of the component whose ``x[i]`` the variables are: the
40
+ # result file names them ``<instance>.x[i]``. Contributed by a recognizer
41
+ # that takes those variables out of a matched component — their count
42
+ # (``n_vars``) or their expressions, either one; a recognizer that
43
+ # contributes neither leaves it None and the conventional default stands.
44
+ unit_tests_instance: str | None = None
45
+ x_expressions: list[str] = field(default_factory=list)
46
+ x_raw: str = ""
47
+ x_reference: list[float] | None = None
48
+ error_expected: float | None = None
49
+ # Simulation-parameters contribution (was ExperimentInfo)
50
+ stop_time: float | None = None
51
+ tolerance: float | None = None
52
+ method: str | None = None
53
+ number_of_intervals: int | None = None
54
+ output_interval: float | None = None
55
+ # PTA.4 — richer-contract runtime-behavior fields. Recognizers can set
56
+ # these; TestModel carries them through. Consumed by PTA.5 (simulate_only)
57
+ # and 4.B's cross-backend chain (requested_baselines).
58
+ simulate_only: bool | None = None
59
+ requested_baselines: list[str] | None = None
60
+
61
+
62
+ class Recognizer(ABC):
63
+ """Base class for source-file recognizers."""
64
+
65
+ #: Unique recognizer name; used in diagnostics and as a registry key.
66
+ name: str = "<unnamed>"
67
+ #: ``Config.source_type`` values this recognizer applies to.
68
+ applies_to: frozenset[str] = frozenset()
69
+
70
+ @abstractmethod
71
+ def recognize(self, source_file: Path) -> RecognizerResult | None:
72
+ """Inspect a source file. Return a result if a test is recognized, else None."""
73
+ ...
74
+
75
+ def applies_to_path(self, source_file: Path, base: Path) -> bool:
76
+ """Optional pre-filter: return False to skip the file before recognize().
77
+
78
+ Default: True (no filtering). Subclasses can override to honor a
79
+ per-recognizer paths_include / paths_exclude config (PTA-follow.1).
80
+ """
81
+ return True
82
+
83
+
84
+ # Registration-ordered list. The bundled recognizer registers first on import;
85
+ # user-provided recognizers (PTA.3) append, and discovery's per-model merge
86
+ # applies later writers as overrides — bundled defaults, user values win.
87
+ _REGISTRY: list[Recognizer] = []
88
+
89
+
90
+ def register(recognizer: Recognizer) -> None:
91
+ """Add a recognizer to the registry (idempotent on identity)."""
92
+ if recognizer not in _REGISTRY:
93
+ _REGISTRY.append(recognizer)
94
+
95
+
96
+ def get_recognizers(source_type: str) -> list[Recognizer]:
97
+ """Return registered recognizers that apply to ``source_type``."""
98
+ return [r for r in _REGISTRY if source_type in r.applies_to]
99
+
100
+
101
+ def _reset_for_tests() -> None:
102
+ """Clear the registry — only for unit tests that need a clean slate."""
103
+ _REGISTRY.clear()
@@ -0,0 +1,154 @@
1
+ """The source scan, cached across tool restarts (brief §3.8).
2
+
3
+ Under ``$XDG_CACHE_HOME/dstf/scan/`` (default ``~/.cache/dstf/scan/``),
4
+ one JSON file per (library path, recognizer set) — a file another DSTF
5
+ version, or other discovery code, wrote is ignored and replaced — never in
6
+ the work dir, which is a run's artifact directory that other tools read,
7
+ and never validated per file: on a 9P mount (``/mnt/d``) a ``stat`` sweep
8
+ over TRANSFORM's 3 269 ``.mo`` files measured 27 s, longer than parsing
9
+ them. The Session opens from the cache and rescans in the background.
10
+ Only ``source_type == "modelica"`` scans anything, so only those cache.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import functools
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import os
21
+ import threading
22
+ from pathlib import Path
23
+
24
+ from .. import __version__
25
+ from ..config import Config
26
+ from . import (
27
+ mo_parser, # noqa: F401 — registers the bundled recognizer; the key names it
28
+ )
29
+ from .recognizer import get_recognizers
30
+ from .test_registry import TestModel
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # The FORMAT of a cache file. What a scan MEANS — what the parser extracts
35
+ # and how it spells it — is not versioned by hand: see _discovery_digest.
36
+ CACHE_VERSION = 2
37
+
38
+ #: The modules that decide what a scan produces. A cache holds parsed
39
+ #: ``TestModel``s, so a change to any of them can make a cached entry mean
40
+ #: something the current code would never have written, and no source file
41
+ #: changed for a staleness check to see. Bumping a constant by hand is a
42
+ #: step people forget — twice already, once for the instance name and once
43
+ #: for expression whitespace — so the code itself is the version.
44
+ _DISCOVERY_SOURCES = (
45
+ "json_recognizer.py",
46
+ "mo_parser.py",
47
+ "recognizer.py",
48
+ "test_registry.py",
49
+ "unit_tests.py",
50
+ )
51
+
52
+
53
+ @functools.lru_cache(maxsize=1)
54
+ def _discovery_digest() -> str:
55
+ """A digest of the discovery code's source bytes.
56
+
57
+ Read once per process: these files do not change under a running tool,
58
+ and a warm ``dstf author`` open asks for this before anything else.
59
+ """
60
+ here = Path(__file__).parent
61
+ h = hashlib.sha256()
62
+ for name in _DISCOVERY_SOURCES:
63
+ h.update(name.encode("utf-8"))
64
+ try:
65
+ h.update((here / name).read_bytes())
66
+ except OSError as e: # a zipped / stripped install: fall back
67
+ logger.warning("cannot digest the discovery source %s: %s", name, e)
68
+ h.update(b"<unreadable>")
69
+ return h.hexdigest()[:16]
70
+
71
+
72
+ def cache_dir() -> Path:
73
+ root = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
74
+ return Path(root) / "dstf" / "scan"
75
+
76
+
77
+ def _key(config: Config) -> str:
78
+ recognizers = sorted(
79
+ r.name
80
+ for r in get_recognizers("modelica")
81
+ if r.name not in config.disabled_bundled
82
+ ) + sorted(r.name for r in config.recognizers if "modelica" in r.applies_to)
83
+ payload = json.dumps(
84
+ [str(config.library_dir.resolve()), recognizers, CACHE_VERSION], sort_keys=True
85
+ )
86
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
87
+
88
+
89
+ def _path(config: Config) -> Path:
90
+ return cache_dir() / f"{_key(config)}.json"
91
+
92
+
93
+ def _to_json(test: TestModel) -> dict:
94
+ if test.metric_tree_spec is not None:
95
+ raise ValueError(f"{test.model_id}: a scanned test carries no metric tree")
96
+ data = dataclasses.asdict(test)
97
+ data["source_file"] = str(test.source_file)
98
+ return data
99
+
100
+
101
+ def _from_json(data: dict) -> TestModel:
102
+ data = dict(data)
103
+ data["source_file"] = Path(data["source_file"])
104
+ return TestModel(**data)
105
+
106
+
107
+ def load_scan(config: Config) -> dict[str, TestModel] | None:
108
+ """The cached scan for ``config``, or None (no cache, another DSTF
109
+ version, other discovery code, unreadable, or not a Modelica source)."""
110
+ if config.source_type != "modelica":
111
+ return None
112
+ path = _path(config)
113
+ try:
114
+ data = json.loads(path.read_text(encoding="utf-8"))
115
+ if data.get("dstf_version") != __version__:
116
+ return None
117
+ if data.get("discovery_digest") != _discovery_digest():
118
+ return None
119
+ return {m: _from_json(d) for m, d in data["tests"].items()}
120
+ except FileNotFoundError:
121
+ return None
122
+ except (OSError, ValueError, TypeError, KeyError) as e:
123
+ logger.warning("ignoring the scan cache %s: %s", path, e)
124
+ return None
125
+
126
+
127
+ def store_scan(config: Config, scanned: dict[str, TestModel]) -> Path | None:
128
+ """Write the scan; returns the path, or None for a non-Modelica source
129
+ or when the cache cannot be written (a read-only home, a full disk).
130
+ The cache is an accelerator: a failed write is a warning, never an
131
+ error — an open that cannot cache still opens, and a rescan that cannot
132
+ cache still re-merges and finishes."""
133
+ if config.source_type != "modelica":
134
+ return None
135
+ path = _path(config)
136
+ payload = {
137
+ "dstf_version": __version__,
138
+ "discovery_digest": _discovery_digest(),
139
+ "library_dir": str(config.library_dir.resolve()),
140
+ "tests": {m: _to_json(t) for m, t in scanned.items()},
141
+ }
142
+ # Unique per thread as well as per process: a cached open's rescan
143
+ # thread and a second Session's in the same process write the same
144
+ # key, and with one name the second write replaced the first's file
145
+ # from under it and its own rename failed.
146
+ tmp = path.with_name(f"{path.stem}.{os.getpid()}.{threading.get_ident()}.tmp")
147
+ try:
148
+ path.parent.mkdir(parents=True, exist_ok=True)
149
+ tmp.write_text(json.dumps(payload), encoding="utf-8", newline="\n")
150
+ os.replace(tmp, path)
151
+ except OSError as e:
152
+ logger.warning("not caching the scan at %s: %s", path, e)
153
+ return None
154
+ return path