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,103 @@
|
|
|
1
|
+
"""Run-level provenance metadata: which backend/version produced
|
|
2
|
+
a set of results.
|
|
3
|
+
|
|
4
|
+
Motivation (2026-07-07): a report showed *what* the verdicts were but never
|
|
5
|
+
*what produced them*. When results drift, the first question is "did the tool
|
|
6
|
+
change?" — e.g. Dymola 2025x vs 2026x, or an MSL bump. Without the backend +
|
|
7
|
+
version stamped on the report, that question needs a full re-investigation.
|
|
8
|
+
|
|
9
|
+
Design: the identity that's always available comes from ``Config`` for free
|
|
10
|
+
(``backend``, the user-configured ``backend_name`` label, ``os_name``).
|
|
11
|
+
The *actual* tool-reported version (``DymolaVersion()``, ``getVersion()``, …)
|
|
12
|
+
is best-effort — captured from a live backend when obtainable, ``None`` when
|
|
13
|
+
not, and never allowed to fail a run. ``tool_version`` is therefore the gold
|
|
14
|
+
standard when present and the configured ``backend_name`` label is the reliable
|
|
15
|
+
floor.
|
|
16
|
+
|
|
17
|
+
This dataclass is constructed once per run by the backend, serialized into
|
|
18
|
+
``status.json`` under a ``"metadata"`` key, and read back by the reporters to
|
|
19
|
+
render a provenance banner.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import asdict, dataclass
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
29
|
+
from ..config import Config
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _dstf_version() -> str:
|
|
33
|
+
"""Installed DSTF version, or ``"unknown"`` when it can't be resolved.
|
|
34
|
+
|
|
35
|
+
Never raises — provenance must not be able to break a run.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
39
|
+
|
|
40
|
+
from .. import _DIST_NAME
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return version(_DIST_NAME)
|
|
44
|
+
except PackageNotFoundError:
|
|
45
|
+
return "unknown"
|
|
46
|
+
except Exception: # pragma: no cover — defensive
|
|
47
|
+
return "unknown"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class RunMetadata:
|
|
52
|
+
"""Provenance for one test run.
|
|
53
|
+
|
|
54
|
+
``backend`` resolved backend family (e.g. ``"Dymola"``).
|
|
55
|
+
``backend_name`` the label the user configured (e.g. ``"Dymola 2026x"``);
|
|
56
|
+
equals ``backend`` when no version suffix was given.
|
|
57
|
+
``os`` ``"linux"`` / ``"macos"`` / ``"windows"``.
|
|
58
|
+
``tool_version`` actual tool-reported version, or ``None`` when the
|
|
59
|
+
backend couldn't (or wasn't asked to) report one.
|
|
60
|
+
``dstf_version`` the DSTF package version that ran the tests.
|
|
61
|
+
``generated_at`` epoch seconds when the run started; ``None`` only in
|
|
62
|
+
synthetic contexts (tests) that don't stamp a time.
|
|
63
|
+
``library_versions`` loaded-library versions the backend could report,
|
|
64
|
+
e.g. ``{"Modelica": "4.1.0"}`` — the Modelica Standard
|
|
65
|
+
Library version is the second drift suspect (a solver's
|
|
66
|
+
MSL can change between tool releases). ``None`` when the
|
|
67
|
+
backend reports none.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
backend: str
|
|
71
|
+
backend_name: str
|
|
72
|
+
os: str
|
|
73
|
+
tool_version: str | None = None
|
|
74
|
+
dstf_version: str = ""
|
|
75
|
+
generated_at: float | None = None
|
|
76
|
+
library_versions: dict | None = None
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_config(
|
|
80
|
+
cls,
|
|
81
|
+
config: Config,
|
|
82
|
+
tool_version: str | None = None,
|
|
83
|
+
now: float | None = None,
|
|
84
|
+
library_versions: dict | None = None,
|
|
85
|
+
) -> RunMetadata:
|
|
86
|
+
"""Build from a resolved ``Config``.
|
|
87
|
+
|
|
88
|
+
``now`` is injectable so tests can assert a deterministic timestamp;
|
|
89
|
+
production passes ``None`` and gets wall-clock ``time.time()``.
|
|
90
|
+
"""
|
|
91
|
+
return cls(
|
|
92
|
+
backend=config.backend,
|
|
93
|
+
backend_name=config.backend_name or config.backend,
|
|
94
|
+
os=config.os_name or "unknown",
|
|
95
|
+
tool_version=tool_version,
|
|
96
|
+
dstf_version=_dstf_version(),
|
|
97
|
+
generated_at=time.time() if now is None else now,
|
|
98
|
+
library_versions=library_versions or None,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def as_dict(self) -> dict:
|
|
102
|
+
"""Plain-dict form for JSON serialization into ``status.json``."""
|
|
103
|
+
return asdict(self)
|
dstf/backends/types.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Value types every backend speaks, and the variable-pattern resolver.
|
|
2
|
+
|
|
3
|
+
Stdlib + numpy only: this module imports nothing from ``dstf``. That is what
|
|
4
|
+
keeps the package's import graph acyclic — :mod:`dstf.storage` and
|
|
5
|
+
:mod:`dstf.comparison` can name these types without a cycle back through the
|
|
6
|
+
backends, and a backend module can import them before the registry in
|
|
7
|
+
:mod:`dstf.backends` has finished loading.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import NamedTuple
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Backend capabilities + dataset types (Phase 1 abstraction)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# See docs/vision.md and docs/extensibility.md. Each backend declares what it
|
|
26
|
+
# supports; framework features gate on these rather than on backend class.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Capability(str, Enum):
|
|
30
|
+
"""Declared abilities of a backend."""
|
|
31
|
+
|
|
32
|
+
#: Backend can hold a loaded model in memory across multiple tests.
|
|
33
|
+
PERSISTENT_WORKERS = "persistent-workers"
|
|
34
|
+
#: Backend exposes a non-interactive script-driven fallback.
|
|
35
|
+
BATCH_FALLBACK = "batch-fallback"
|
|
36
|
+
#: Backend can export a test artefact as an FMU (enables cross-backend verification).
|
|
37
|
+
FMU_EXPORT = "fmu-export"
|
|
38
|
+
#: The result file grows while the run is in progress and can be read
|
|
39
|
+
#: then: ``Backend.live_result_path`` says which file.
|
|
40
|
+
LIVE_RESULTS = "live-results"
|
|
41
|
+
#: Backend can stop a test that is running, from another thread (the
|
|
42
|
+
#: simulation queue's cancel).
|
|
43
|
+
CANCEL = "cancel"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DatasetType(str, Enum):
|
|
47
|
+
"""Typed categories of result data a backend may produce.
|
|
48
|
+
|
|
49
|
+
Every backend currently emits ``TIME_SERIES``. ``SCALARS`` is scorable
|
|
50
|
+
already — the ``scalar`` metric accepts it — so a scalars-only backend
|
|
51
|
+
would work today; the remaining types (events, spectra, distributions)
|
|
52
|
+
are reserved for metrics that don't exist yet.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
TIME_SERIES = "time-series"
|
|
56
|
+
SCALARS = "scalars"
|
|
57
|
+
EVENTS = "events"
|
|
58
|
+
SPECTRUM = "spectrum"
|
|
59
|
+
DISTRIBUTION = "distribution"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Common result types (shared across all backends)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class VariableResult:
|
|
69
|
+
"""Time series for a single tracked variable."""
|
|
70
|
+
|
|
71
|
+
index: int # 1-based
|
|
72
|
+
time: np.ndarray
|
|
73
|
+
values: np.ndarray
|
|
74
|
+
# The variable's name in the result file ("pipe.T[1]", "unitTests.x[1]")
|
|
75
|
+
# — the only key: overrides, tree leaves, plot series and baselines all
|
|
76
|
+
# name a variable this way.
|
|
77
|
+
name: str = ""
|
|
78
|
+
# What the model wrote for a UnitTests variable (``x={pipe.T[1]}``): a
|
|
79
|
+
# label for people, recorded beside the name in the baseline. Never a key,
|
|
80
|
+
# and empty for a variable whose name is all there is.
|
|
81
|
+
expression: str = ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def display_label(name: str, expression: str) -> str:
|
|
85
|
+
"""What a person reads for a variable: the expression the model wrote
|
|
86
|
+
for it, or its name when it wrote none.
|
|
87
|
+
|
|
88
|
+
The plain label. A surface that shows the muted NAME beside its title
|
|
89
|
+
— the report's variable heading, the tool's card title — reads this;
|
|
90
|
+
every other human-facing surface reads :func:`variable_texts`' ``text``
|
|
91
|
+
instead, which adds the name where the label is ambiguous. The name
|
|
92
|
+
remains the key everywhere — element ids, markers, ``meta.id``,
|
|
93
|
+
``data-variable``, overrides, leaves, baselines — so nothing is ever
|
|
94
|
+
looked up by either.
|
|
95
|
+
"""
|
|
96
|
+
return expression or name
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def label_with_name(name: str, expression: str) -> str:
|
|
100
|
+
"""``label (name)`` for the LOG surfaces — the console, JUnit, the
|
|
101
|
+
interactive review — and the bare name when the two agree.
|
|
102
|
+
|
|
103
|
+
These three do NOT take the page's rule (:func:`variable_texts`): a
|
|
104
|
+
line of CI output is read without a page around it, so there is no
|
|
105
|
+
scope to disambiguate within. The expression says WHICH quantity and
|
|
106
|
+
the name says what to write a criterion, an override or a `--filter`
|
|
107
|
+
against, every time the two differ — whether or not some other
|
|
108
|
+
variable of the same test happens to share the expression.
|
|
109
|
+
"""
|
|
110
|
+
label = display_label(name, expression)
|
|
111
|
+
return f"{label} ({name})" if label != name else name
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class VariableText(NamedTuple):
|
|
115
|
+
"""What one variable reads as on a page, both ways.
|
|
116
|
+
|
|
117
|
+
``label`` is :func:`display_label` — the expression the model wrote,
|
|
118
|
+
else the name. ``text`` is that label with the name beside it when the
|
|
119
|
+
page's scope makes the label ambiguous. A surface that already shows
|
|
120
|
+
the muted name beside its title reads ``label``; every other
|
|
121
|
+
human-facing surface reads ``text``. See :func:`variable_texts`.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
label: str
|
|
125
|
+
text: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def variable_texts(
|
|
129
|
+
labels: Mapping[str, str],
|
|
130
|
+
plot_series: Iterable[str] = (),
|
|
131
|
+
) -> dict[str, VariableText]:
|
|
132
|
+
"""``{name: VariableText}`` — the ONE rule for what a person reads for
|
|
133
|
+
each variable of a TEST, on both tiers.
|
|
134
|
+
|
|
135
|
+
The scope is the whole test: every name its page can show. ``labels``
|
|
136
|
+
maps name → :func:`display_label` for the result's tracked variables
|
|
137
|
+
and the scored comparisons; ``plot_series`` adds the names of every
|
|
138
|
+
series of the test's resolved plots, a plot-only one included — such a
|
|
139
|
+
name has no expression, so its label is itself.
|
|
140
|
+
|
|
141
|
+
``text`` is the label, unless ANOTHER name in that scope carries the
|
|
142
|
+
same label and the label is not already the name — then the name rides
|
|
143
|
+
with it, the same ``label (name)`` pair :func:`label_with_name`
|
|
144
|
+
prints. ``x={a.Q, a.Q}`` is legal Modelica and happens: two criteria
|
|
145
|
+
rows, two legend entries, two pills or two select options reading
|
|
146
|
+
``a.Q`` name two different variables and give no way to tell which is
|
|
147
|
+
which. The ``label != name`` half is what keeps a plot-only variable
|
|
148
|
+
genuinely NAMED ``z`` reading ``z`` beside a tracked variable whose
|
|
149
|
+
EXPRESSION is ``z`` — the one carrying the expression is the ambiguous
|
|
150
|
+
one, and it reads ``z (unitTests.x[1])``.
|
|
151
|
+
|
|
152
|
+
One scope for the test, not one per plot or one per scored list: five
|
|
153
|
+
different scopes gave the same variable four different texts on one
|
|
154
|
+
page (the b147918 review).
|
|
155
|
+
"""
|
|
156
|
+
scope: dict[str, str] = {name: name for name in plot_series if name}
|
|
157
|
+
scope.update({name: label for name, label in labels.items() if name})
|
|
158
|
+
counts = Counter(scope.values())
|
|
159
|
+
return {
|
|
160
|
+
name: VariableText(
|
|
161
|
+
label,
|
|
162
|
+
f"{label} ({name})" if label != name and counts[label] > 1 else label,
|
|
163
|
+
)
|
|
164
|
+
for name, label in scope.items()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class TestResult:
|
|
170
|
+
"""Results from a single test simulation."""
|
|
171
|
+
|
|
172
|
+
model_id: str
|
|
173
|
+
success: bool
|
|
174
|
+
variables: list[VariableResult] = field(default_factory=list)
|
|
175
|
+
diagnostics: list[VariableResult] = field(default_factory=list)
|
|
176
|
+
error_message: str | None = None
|
|
177
|
+
statistics: dict | None = None
|
|
178
|
+
#: Whether ``error_message`` is the RUN's own reason — its run result's,
|
|
179
|
+
#: or a failure the run wrote into its result file — and not a reader's
|
|
180
|
+
#: account of what the file holds. A reader's account is led by the
|
|
181
|
+
#: verdict the run recorded (``backends.base._force_failed``); the run's
|
|
182
|
+
#: own reason already says what happened. In memory only.
|
|
183
|
+
run_reason: bool = False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
#: Appended to every timed-out result's ``error_message``, so the escape hatch
|
|
187
|
+
#: travels with the failure to wherever it is read — the dispatcher's progress
|
|
188
|
+
#: line, the console report, the dashboard detail. ``--timeout`` only moves the
|
|
189
|
+
#: global default; one genuinely slow model wants the per-test override, and a
|
|
190
|
+
#: user who has just hit a timeout is exactly who needs to be told that.
|
|
191
|
+
TIMEOUT_OVERRIDE_HINT = (
|
|
192
|
+
"raise it per test with simulation.timeout in test_spec.json, "
|
|
193
|
+
"or globally with --timeout"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def timeout_message(what: str) -> str:
|
|
198
|
+
"""``"<what> (<hint>)"`` — the one phrasing for a timeout failure."""
|
|
199
|
+
return f"{what} ({TIMEOUT_OVERRIDE_HINT})"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class TestRunResult:
|
|
204
|
+
"""Result of running a single test."""
|
|
205
|
+
|
|
206
|
+
model_id: str
|
|
207
|
+
test_key: str
|
|
208
|
+
success: bool
|
|
209
|
+
elapsed: float = 0.0
|
|
210
|
+
error_message: str | None = None
|
|
211
|
+
timed_out: bool = False
|
|
212
|
+
statistics: dict | None = None
|
|
213
|
+
# Phase breakdown (captured by the persistent backend; None in batch mode)
|
|
214
|
+
translation_wall: float | None = None
|
|
215
|
+
sim_wall: float | None = None
|
|
216
|
+
# Set by persistent workers when the test run left the worker process
|
|
217
|
+
# dead (we called close()). The dispatch loop uses this to decide
|
|
218
|
+
# whether the next restart should count against the budget — workers
|
|
219
|
+
# killed for benign reasons (timeout, post-timeout disk-rescue) get a
|
|
220
|
+
# fresh start without ticking the bound. Independent of `timed_out`
|
|
221
|
+
# and `success`: a disk-rescued test can have success=True AND
|
|
222
|
+
# worker_killed=True (the .mat was written just before the watchdog
|
|
223
|
+
# killed Dymola).
|
|
224
|
+
worker_killed: bool = False
|
|
225
|
+
# The run was stopped on request (the simulation queue's cancel), so it
|
|
226
|
+
# is neither a failure nor a result: recorded as cancelled
|
|
227
|
+
# (BatchManifest.record_result) and read back as no result at all.
|
|
228
|
+
# ``success`` is False beside it.
|
|
229
|
+
cancelled: bool = False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# Variables to exclude when resolving "*" or glob patterns (Dymola internals, etc.)
|
|
233
|
+
_EXCLUDE_PREFIXES = ("der(", "$", "_")
|
|
234
|
+
_EXCLUDE_NAMES = {"time"}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _pattern_to_regex(pattern: str):
|
|
238
|
+
"""Convert a simple glob pattern to a compiled regex.
|
|
239
|
+
|
|
240
|
+
Only * (match any) and ? (match one) are wildcards.
|
|
241
|
+
All other characters (including Modelica's [] for array indices,
|
|
242
|
+
parentheses, dots) are treated as literals.
|
|
243
|
+
"""
|
|
244
|
+
import re as _re
|
|
245
|
+
|
|
246
|
+
parts = []
|
|
247
|
+
for ch in pattern:
|
|
248
|
+
if ch == "*":
|
|
249
|
+
parts.append(".*")
|
|
250
|
+
elif ch == "?":
|
|
251
|
+
parts.append(".")
|
|
252
|
+
else:
|
|
253
|
+
parts.append(_re.escape(ch))
|
|
254
|
+
return _re.compile("".join(parts))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def is_glob_pattern(pattern: str) -> bool:
|
|
258
|
+
"""Only ``*`` and ``?`` are wildcards; everything else — including the
|
|
259
|
+
``[]`` of a Modelica array element such as ``pipe.T[1]`` — is literal.
|
|
260
|
+
The one rule :func:`resolve_variable_patterns` and the spec-update
|
|
261
|
+
materialization share."""
|
|
262
|
+
return "*" in pattern or "?" in pattern
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def resolve_variable_patterns(
|
|
266
|
+
patterns: list[str],
|
|
267
|
+
available_vars: list[str],
|
|
268
|
+
) -> list[str]:
|
|
269
|
+
"""Resolve variable patterns against available variable names from simulation.
|
|
270
|
+
|
|
271
|
+
Pattern types:
|
|
272
|
+
- Explicit name: "pipe.T[1]" — must exist exactly
|
|
273
|
+
- Glob: "medium.T*" — fnmatch against available names
|
|
274
|
+
- Wildcard: "*" — all non-internal variables
|
|
275
|
+
- Empty list: [] — no variables (simulate-only mode)
|
|
276
|
+
|
|
277
|
+
Returns the resolved names in PATTERN order: explicit names as written,
|
|
278
|
+
each glob's matches in ``available_vars``' (the result file's) order, a
|
|
279
|
+
name matched twice where it was first. Sorted, ``x[10]`` came before
|
|
280
|
+
``x[2]``: a test with no tree drew its plots alphabetically, and an
|
|
281
|
+
accept wrote its ``variables`` that way.
|
|
282
|
+
"""
|
|
283
|
+
if not patterns:
|
|
284
|
+
return []
|
|
285
|
+
|
|
286
|
+
resolved: dict[str, None] = {}
|
|
287
|
+
for pattern in patterns:
|
|
288
|
+
if pattern == "*":
|
|
289
|
+
# All non-internal variables
|
|
290
|
+
for var in available_vars:
|
|
291
|
+
if var in _EXCLUDE_NAMES:
|
|
292
|
+
continue
|
|
293
|
+
if any(var.startswith(p) for p in _EXCLUDE_PREFIXES):
|
|
294
|
+
continue
|
|
295
|
+
resolved.setdefault(var)
|
|
296
|
+
elif is_glob_pattern(pattern):
|
|
297
|
+
regex = _pattern_to_regex(pattern)
|
|
298
|
+
matched = [v for v in available_vars if regex.fullmatch(v)]
|
|
299
|
+
if not matched:
|
|
300
|
+
logger.warning("Pattern '%s' matched no variables", pattern)
|
|
301
|
+
for var in matched:
|
|
302
|
+
resolved.setdefault(var)
|
|
303
|
+
else:
|
|
304
|
+
# Explicit name
|
|
305
|
+
if pattern in available_vars:
|
|
306
|
+
resolved.setdefault(pattern)
|
|
307
|
+
else:
|
|
308
|
+
logger.warning("Variable '%s' not found in results", pattern)
|
|
309
|
+
|
|
310
|
+
return list(resolved)
|
dstf/cli/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Command-line interface for the Dynamic Systems Testing Framework.
|
|
2
|
+
|
|
3
|
+
The package's public face is the two entry points: ``main`` (used by the
|
|
4
|
+
docs and by tests that drive the whole CLI) and ``main_entry`` (the
|
|
5
|
+
``[project.scripts]`` console-script target and what ``python -m dstf``
|
|
6
|
+
calls). Everything else lives in the command modules and is imported from
|
|
7
|
+
its own home — there is no compatibility re-export layer here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .main import main, main_entry
|
|
11
|
+
|
|
12
|
+
__all__ = ["main", "main_entry"]
|
dstf/cli/author_cmd.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""`dstf author`: the authoring tool (§4.7 "Launch")."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..config import ConfigError
|
|
8
|
+
|
|
9
|
+
#: Every module the ``[author]`` extra installs, checked before serving:
|
|
10
|
+
#: the Variables drawer's `regex` is reached only when a Test page is (per
|
|
11
|
+
#: request), so a venv synced before it joined the extra served the Suite
|
|
12
|
+
#: page and failed every Test page. The extra's distributions are these
|
|
13
|
+
#: names (`test_author_checks_every_package_of_the_extra`).
|
|
14
|
+
_AUTHOR_MODULES = ("nicegui", "regex")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_author(args: argparse.Namespace) -> int:
|
|
18
|
+
"""Open the authoring shell on this project and serve until Quit."""
|
|
19
|
+
# The Session builds the Config itself, from the file it will save
|
|
20
|
+
# beside. Nothing is resolved before these checks: a full Config
|
|
21
|
+
# resolution can WRITE a default testing.json next to a package.mo.
|
|
22
|
+
if not getattr(args, "config", None):
|
|
23
|
+
raise ConfigError("dstf author needs --config <testing.json>")
|
|
24
|
+
dropped = [
|
|
25
|
+
flag
|
|
26
|
+
for attr, flag in (
|
|
27
|
+
("source_path", "--source-path"),
|
|
28
|
+
("reference_root", "--reference-root"),
|
|
29
|
+
("test_spec", "--test-spec"),
|
|
30
|
+
("dymola_interface", "--dymola-interface"),
|
|
31
|
+
)
|
|
32
|
+
if getattr(args, attr, None)
|
|
33
|
+
]
|
|
34
|
+
if dropped:
|
|
35
|
+
raise ConfigError(
|
|
36
|
+
f"dstf author reads {', '.join(dropped)} from testing.json; the tool "
|
|
37
|
+
"edits and saves the spec that file names, so an override here "
|
|
38
|
+
"would save to a different file than it shows"
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
for module in _AUTHOR_MODULES:
|
|
42
|
+
importlib.import_module(module)
|
|
43
|
+
from ..authoring.shell import run_app
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
# The tool needs NiceGUI and regex, the optional extra — say how to
|
|
46
|
+
# get it as one clean "Config error: ..." line, never a traceback.
|
|
47
|
+
raise ConfigError(
|
|
48
|
+
"the authoring tool needs the [author] extra: "
|
|
49
|
+
"uv sync --extra dev --extra author (or pip install "
|
|
50
|
+
f"'dynamic-systems-testing-framework[author]') — {e}"
|
|
51
|
+
) from e
|
|
52
|
+
run_app(
|
|
53
|
+
Path(args.config),
|
|
54
|
+
filter=args.filter,
|
|
55
|
+
package=args.package,
|
|
56
|
+
port=args.port,
|
|
57
|
+
work_dir=Path(args.work_dir) if getattr(args, "work_dir", None) else None,
|
|
58
|
+
backend=args.backend if getattr(args, "backend", None) else None,
|
|
59
|
+
show=not args.no_browser,
|
|
60
|
+
)
|
|
61
|
+
return 0
|