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,192 @@
|
|
|
1
|
+
"""JUnit XML report generation for CI integration."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..comparison.comparator import TestComparison
|
|
9
|
+
from .status import (
|
|
10
|
+
FAIL,
|
|
11
|
+
NO_REF,
|
|
12
|
+
NOT_RUN,
|
|
13
|
+
SIM_FAIL,
|
|
14
|
+
advisory_failures,
|
|
15
|
+
advisory_text,
|
|
16
|
+
classify,
|
|
17
|
+
count_by_status,
|
|
18
|
+
failed_criteria,
|
|
19
|
+
group_failures,
|
|
20
|
+
refusal,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
#: Every character XML 1.0 cannot carry (its ``Char`` production): a control
|
|
24
|
+
#: character below U+0020 but tab, LF and CR, a surrogate — in a ``str``
|
|
25
|
+
#: every one is a lone one — U+FFFE and U+FFFF.
|
|
26
|
+
_NOT_XML = re.compile("[^\t\n\r\x20-\ud7ff\ue000-\ufffd\U00010000-\U0010ffff]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _xml_text(text: str) -> str:
|
|
30
|
+
"""``text`` as an XML 1.0 file can hold it: each character it cannot —
|
|
31
|
+
the ESC of an ANSI colour code in a simulator's message, a NUL, a lone
|
|
32
|
+
surrogate — as U+FFFD. There is no escape to write instead: ````
|
|
33
|
+
is as ill-formed as the character, and no parser reads a file that
|
|
34
|
+
holds either."""
|
|
35
|
+
return _NOT_XML.sub("\ufffd", text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _failure_message(failed: list[str], groups: list[str], advisory: list[str]) -> str:
|
|
39
|
+
"""`1 criterion failed`, and `, with 1 advisory failure` when some
|
|
40
|
+
comparison under a `warn` failed beside it. With no failing criterion
|
|
41
|
+
the test failed at a group, and the message is that group's line (`2
|
|
42
|
+
groups failed` for more than one; the body has each)."""
|
|
43
|
+
n = len(failed)
|
|
44
|
+
if n == 0 and groups:
|
|
45
|
+
text = groups[0] if len(groups) == 1 else f"{len(groups)} groups failed"
|
|
46
|
+
else:
|
|
47
|
+
text = f"{n} {'criterion' if n == 1 else 'criteria'} failed"
|
|
48
|
+
if advisory:
|
|
49
|
+
text += f", with {advisory_text(len(advisory))}"
|
|
50
|
+
return text
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_junit_report(
|
|
54
|
+
comparisons: list[TestComparison],
|
|
55
|
+
output_path: Path,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Generate a JUnit XML report from test comparisons."""
|
|
58
|
+
suites: dict[str, list[TestComparison]] = defaultdict(list)
|
|
59
|
+
for comp in comparisons:
|
|
60
|
+
parts = comp.model_id.split(".")
|
|
61
|
+
suite_name = ".".join(parts[:2]) if len(parts) >= 2 else parts[0]
|
|
62
|
+
suites[suite_name].append(comp)
|
|
63
|
+
|
|
64
|
+
root = ET.Element("testsuites")
|
|
65
|
+
total_tests = 0
|
|
66
|
+
total_failures = 0
|
|
67
|
+
total_errors = 0
|
|
68
|
+
|
|
69
|
+
for suite_name in sorted(suites.keys()):
|
|
70
|
+
suite_comps = suites[suite_name]
|
|
71
|
+
n_tests = len(suite_comps)
|
|
72
|
+
# Buckets are the shared ones from reporting.status, so the JUnit
|
|
73
|
+
# counters can't drift from the console's — review 2026-07-06,
|
|
74
|
+
# finding 5: failing baseline-free tests were emitted as <skipped>
|
|
75
|
+
# with failures="0".
|
|
76
|
+
counts = count_by_status(suite_comps)
|
|
77
|
+
n_failures = counts[FAIL]
|
|
78
|
+
n_errors = counts[SIM_FAIL]
|
|
79
|
+
n_skipped = counts[NO_REF] + counts[NOT_RUN]
|
|
80
|
+
|
|
81
|
+
suite_elem = ET.SubElement(
|
|
82
|
+
root,
|
|
83
|
+
"testsuite",
|
|
84
|
+
{
|
|
85
|
+
"name": suite_name,
|
|
86
|
+
"tests": str(n_tests),
|
|
87
|
+
"failures": str(n_failures),
|
|
88
|
+
"errors": str(n_errors),
|
|
89
|
+
"skipped": str(n_skipped),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
for comp in suite_comps:
|
|
94
|
+
tc = ET.SubElement(
|
|
95
|
+
suite_elem,
|
|
96
|
+
"testcase",
|
|
97
|
+
{
|
|
98
|
+
"name": comp.model_id,
|
|
99
|
+
"classname": suite_name,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
state = classify(comp)
|
|
104
|
+
if state == NOT_RUN:
|
|
105
|
+
# No result: the test was never run, or its run never
|
|
106
|
+
# finished it. Not a failure and not an error (D145).
|
|
107
|
+
ET.SubElement(tc, "skipped", {"message": "not run"})
|
|
108
|
+
elif state == SIM_FAIL:
|
|
109
|
+
ET.SubElement(
|
|
110
|
+
tc,
|
|
111
|
+
"error",
|
|
112
|
+
{
|
|
113
|
+
"message": comp.error_message or "Simulation failed",
|
|
114
|
+
"type": "SimulationError",
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
elif state == NO_REF:
|
|
118
|
+
# No baseline AND nothing evaluated — a genuinely skipped
|
|
119
|
+
# test. Failing baseline-free tests classify as FAIL and
|
|
120
|
+
# land in the <failure> branch below.
|
|
121
|
+
ET.SubElement(
|
|
122
|
+
tc,
|
|
123
|
+
"skipped",
|
|
124
|
+
{
|
|
125
|
+
"message": "No reference baseline stored",
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
elif state == FAIL:
|
|
129
|
+
# One line per criterion that did not pass, in the words
|
|
130
|
+
# of the console's block (`failed_criteria`): which
|
|
131
|
+
# criterion it is — two on one variable are two of them —
|
|
132
|
+
# and the measured value against what it is held to, or
|
|
133
|
+
# why it was not scored. A failure under `warn` cannot
|
|
134
|
+
# fail the test: it is not counted with the ones that do,
|
|
135
|
+
# and its line says so.
|
|
136
|
+
criteria = failed_criteria(comp)
|
|
137
|
+
failed = [c.line for c in criteria if not c.advisory]
|
|
138
|
+
advisory = [c.line for c in criteria if c.advisory]
|
|
139
|
+
# A test the comparison refused has no failing criterion to
|
|
140
|
+
# count: its reason is the message and the body.
|
|
141
|
+
refused = refusal(comp)
|
|
142
|
+
# A group that fails on its own — a weighted sum over its
|
|
143
|
+
# threshold — fails the test with no criterion to list.
|
|
144
|
+
groups = group_failures(comp)
|
|
145
|
+
fail = ET.SubElement(
|
|
146
|
+
tc,
|
|
147
|
+
"failure",
|
|
148
|
+
{
|
|
149
|
+
"message": refused
|
|
150
|
+
or _failure_message(failed, groups, advisory),
|
|
151
|
+
"type": "RegressionFailure",
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
fail.text = refused or "\n".join(failed + groups + advisory)
|
|
155
|
+
else:
|
|
156
|
+
# PASS — and it may have passed with a failure under `warn`:
|
|
157
|
+
# no <failure>, counted in nothing, and said, where it used
|
|
158
|
+
# to say nothing at all.
|
|
159
|
+
passed_with = advisory_failures(comp)
|
|
160
|
+
if passed_with:
|
|
161
|
+
n = len(passed_with)
|
|
162
|
+
they = "it" if n == 1 else "they"
|
|
163
|
+
said = ET.SubElement(tc, "system-out")
|
|
164
|
+
said.text = "\n".join(
|
|
165
|
+
[
|
|
166
|
+
f"{advisory_text(n)} (under `warn`: {they} cannot "
|
|
167
|
+
"fail the test)"
|
|
168
|
+
]
|
|
169
|
+
+ [c.line for c in passed_with]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
total_tests += n_tests
|
|
173
|
+
total_failures += n_failures
|
|
174
|
+
total_errors += n_errors
|
|
175
|
+
|
|
176
|
+
root.set("tests", str(total_tests))
|
|
177
|
+
root.set("failures", str(total_failures))
|
|
178
|
+
root.set("errors", str(total_errors))
|
|
179
|
+
|
|
180
|
+
# THE place text enters the file: every attribute value and every
|
|
181
|
+
# element's text, whichever branch above wrote it — a model's name, a
|
|
182
|
+
# simulator's message, a criterion's line with its variable's.
|
|
183
|
+
for element in root.iter():
|
|
184
|
+
for key, value in list(element.attrib.items()):
|
|
185
|
+
element.set(key, _xml_text(value))
|
|
186
|
+
if element.text:
|
|
187
|
+
element.text = _xml_text(element.text)
|
|
188
|
+
|
|
189
|
+
tree = ET.ElementTree(root)
|
|
190
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
ET.indent(tree, space=" ")
|
|
192
|
+
tree.write(str(output_path), encoding="unicode", xml_declaration=True)
|