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,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)