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
dstf/plugins.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Entry-point plug-in loading — the seam third-party packages register through.
|
|
2
|
+
|
|
3
|
+
Every extension registry in the framework (``backends.register``,
|
|
4
|
+
``discovery.recognizer.register``, ``comparison.registry.register_metric`` /
|
|
5
|
+
``register_combinator``) is populated by *importing* the module that calls it.
|
|
6
|
+
Builtins are imported by their package ``__init__``; a third-party plug-in
|
|
7
|
+
lives in a package we never import, so it needs a hook. That hook is a single
|
|
8
|
+
entry-point group:
|
|
9
|
+
|
|
10
|
+
.. code-block:: toml
|
|
11
|
+
|
|
12
|
+
# in the plug-in package's pyproject.toml
|
|
13
|
+
[project.entry-points."dstf.plugins"]
|
|
14
|
+
acme_backend = "acme_dstf.backend" # module: import side effects register
|
|
15
|
+
acme_metrics = "acme_dstf.metrics:register" # callable: invoked with no args
|
|
16
|
+
|
|
17
|
+
One group, not one per layer: a plug-in package routinely ships a backend
|
|
18
|
+
*and* the metrics that score it, and splitting the declaration by layer only
|
|
19
|
+
makes the author guess which group a given registration belongs to.
|
|
20
|
+
|
|
21
|
+
A plug-in that fails to import or call is a **hard error**
|
|
22
|
+
(:class:`~dstf.config.ConfigError`). A metric that silently failed to register
|
|
23
|
+
does not disappear quietly — it changes scoring semantics, either by failing
|
|
24
|
+
spec parse with a confusing "unknown metric" or by falling back to a default.
|
|
25
|
+
Fail at startup, naming the entry point.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import importlib.metadata
|
|
31
|
+
from types import ModuleType
|
|
32
|
+
|
|
33
|
+
#: The single entry-point group third-party packages advertise plug-ins under.
|
|
34
|
+
PLUGIN_GROUP = "dstf.plugins"
|
|
35
|
+
|
|
36
|
+
# None until the first successful load. Loading is idempotent: the CLI calls
|
|
37
|
+
# it once per process, but a library user may call it again from anywhere.
|
|
38
|
+
_loaded: list[str] | None = None
|
|
39
|
+
_completed: set[str] = set()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_plugins() -> list[str]:
|
|
43
|
+
"""Import every ``dstf.plugins`` entry point, returning the names loaded.
|
|
44
|
+
|
|
45
|
+
Each entry point's ``.load()`` result is either a **module** (the import
|
|
46
|
+
itself performed the registrations) or a **callable** (invoked with no
|
|
47
|
+
arguments). Anything else is an error.
|
|
48
|
+
|
|
49
|
+
Idempotent — the second and later calls return the same names without
|
|
50
|
+
re-importing. Raises :class:`~dstf.config.ConfigError` naming the failing
|
|
51
|
+
entry point, chaining the original exception.
|
|
52
|
+
"""
|
|
53
|
+
global _loaded
|
|
54
|
+
if _loaded is not None:
|
|
55
|
+
return list(_loaded)
|
|
56
|
+
|
|
57
|
+
# Per-entry-point completion tracking, not one all-or-nothing flag: a
|
|
58
|
+
# library user who catches the ConfigError from a broken plug-in and
|
|
59
|
+
# retries must not re-invoke the HEALTHY plug-ins that already ran —
|
|
60
|
+
# their re-registration would raise the registry's duplicate error and
|
|
61
|
+
# blame the wrong plug-in (B2 review finding 4).
|
|
62
|
+
names: list[str] = []
|
|
63
|
+
for ep in importlib.metadata.entry_points(group=PLUGIN_GROUP):
|
|
64
|
+
if ep.name in _completed:
|
|
65
|
+
names.append(ep.name)
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
obj = ep.load()
|
|
69
|
+
if isinstance(obj, ModuleType):
|
|
70
|
+
pass # import side effects did the registration
|
|
71
|
+
elif isinstance(obj, type):
|
|
72
|
+
# A class is callable, but instantiating it with no args is
|
|
73
|
+
# never what a plug-in author meant (B2 review finding 8).
|
|
74
|
+
raise TypeError(
|
|
75
|
+
f"resolved to class {obj.__name__}; point the entry "
|
|
76
|
+
f"point at a module or a no-argument callable"
|
|
77
|
+
)
|
|
78
|
+
elif callable(obj):
|
|
79
|
+
obj()
|
|
80
|
+
else:
|
|
81
|
+
raise TypeError(
|
|
82
|
+
f"resolved to {type(obj).__name__}, expected a module or a callable"
|
|
83
|
+
)
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
# Lazy — config imports enough of the package that a module-level
|
|
86
|
+
# import here would risk a cycle, and this is the cold path.
|
|
87
|
+
from .config import ConfigError
|
|
88
|
+
|
|
89
|
+
raise ConfigError(
|
|
90
|
+
f"Failed to load DSTF plug-in '{ep.name}' "
|
|
91
|
+
f"({PLUGIN_GROUP} = {ep.value!r}): {exc}"
|
|
92
|
+
) from exc
|
|
93
|
+
_completed.add(ep.name)
|
|
94
|
+
names.append(ep.name)
|
|
95
|
+
|
|
96
|
+
_loaded = names
|
|
97
|
+
return list(names)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _reset_for_tests() -> None:
|
|
101
|
+
"""Forget that plug-ins were loaded — unit tests only."""
|
|
102
|
+
global _loaded
|
|
103
|
+
_completed.clear()
|
|
104
|
+
_loaded = None
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Console (terminal) report for test comparison results."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from ..comparison.comparator import TestComparison
|
|
6
|
+
from .status import (
|
|
7
|
+
FAIL,
|
|
8
|
+
NO_REF,
|
|
9
|
+
NOT_RUN,
|
|
10
|
+
SIM_FAIL,
|
|
11
|
+
STATUS_LABELS,
|
|
12
|
+
FailedCriterion,
|
|
13
|
+
advisory_failures,
|
|
14
|
+
advisory_text,
|
|
15
|
+
classify,
|
|
16
|
+
exit_code,
|
|
17
|
+
failed_criteria,
|
|
18
|
+
group_failures,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# ANSI color codes
|
|
22
|
+
_GREEN = "\033[92m"
|
|
23
|
+
_RED = "\033[91m"
|
|
24
|
+
_YELLOW = "\033[93m"
|
|
25
|
+
_GREY = "\033[90m"
|
|
26
|
+
_RESET = "\033[0m"
|
|
27
|
+
_BOLD = "\033[1m"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _color(text: str, code: str) -> str:
|
|
31
|
+
"""Wrap text in ANSI color if stdout is a terminal."""
|
|
32
|
+
if sys.stdout.isatty():
|
|
33
|
+
return f"{code}{text}{_RESET}"
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_report(comparisons: list[TestComparison]) -> int:
|
|
38
|
+
"""Print a console report and return exit code (0 when nothing FAILED or
|
|
39
|
+
failed to simulate — NOT RUN and NO_REF do not fail it — and something
|
|
40
|
+
ran, else 1: ``exit_code``)."""
|
|
41
|
+
if not comparisons:
|
|
42
|
+
print("No comparisons to report.")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
n_passed = 0
|
|
46
|
+
n_failed = 0
|
|
47
|
+
n_no_ref = 0
|
|
48
|
+
n_sim_fail = 0
|
|
49
|
+
n_not_run = 0
|
|
50
|
+
failures: list[TestComparison] = []
|
|
51
|
+
warned: list[TestComparison] = []
|
|
52
|
+
advised: list[tuple[TestComparison, list[FailedCriterion]]] = []
|
|
53
|
+
|
|
54
|
+
for comp in comparisons:
|
|
55
|
+
# Ordering (SIM_FAIL > FAIL > NO_REF > PASS) and its rationale live
|
|
56
|
+
# in reporting.status — this is one of its call sites, not a copy.
|
|
57
|
+
state = classify(comp)
|
|
58
|
+
if state == NOT_RUN:
|
|
59
|
+
# No result: nothing ran, so nothing failed (D145).
|
|
60
|
+
n_not_run += 1
|
|
61
|
+
status = _color(STATUS_LABELS[NOT_RUN], _GREY)
|
|
62
|
+
elif state == SIM_FAIL:
|
|
63
|
+
n_sim_fail += 1
|
|
64
|
+
status = _color("SIM_FAIL", _RED)
|
|
65
|
+
elif state == FAIL:
|
|
66
|
+
n_failed += 1
|
|
67
|
+
status = _color("FAIL", _RED)
|
|
68
|
+
elif state == NO_REF:
|
|
69
|
+
n_no_ref += 1
|
|
70
|
+
status = _color("NO_REF", _YELLOW)
|
|
71
|
+
else:
|
|
72
|
+
n_passed += 1
|
|
73
|
+
status = _color("PASS", _GREEN)
|
|
74
|
+
|
|
75
|
+
# Show warning indicator if structural changes detected
|
|
76
|
+
warn_tag = ""
|
|
77
|
+
if comp.warnings:
|
|
78
|
+
warn_tag = _color(" [WARN]", _YELLOW)
|
|
79
|
+
warned.append(comp)
|
|
80
|
+
|
|
81
|
+
if comp.test_id:
|
|
82
|
+
id_tag = f"[{comp.test_id}] "
|
|
83
|
+
elif not comp.has_reference:
|
|
84
|
+
id_tag = "[new] "
|
|
85
|
+
else:
|
|
86
|
+
id_tag = ""
|
|
87
|
+
# A test that passes with a failure under `warn` says so: it read a
|
|
88
|
+
# plain PASS, with no word of the criterion that failed.
|
|
89
|
+
advisory = advisory_failures(comp)
|
|
90
|
+
advisory_tag = ""
|
|
91
|
+
if advisory:
|
|
92
|
+
advised.append((comp, advisory))
|
|
93
|
+
advisory_tag = _color(f" ({advisory_text(len(advisory))})", _YELLOW)
|
|
94
|
+
print(f" {status} {id_tag}{comp.model_id}{advisory_tag}{warn_tag}")
|
|
95
|
+
|
|
96
|
+
if not comp.passed and comp.sim_success:
|
|
97
|
+
failures.append(comp)
|
|
98
|
+
|
|
99
|
+
# Summary
|
|
100
|
+
total = len(comparisons)
|
|
101
|
+
print()
|
|
102
|
+
print(_color(f"Results: {n_passed}/{total} passed", _BOLD))
|
|
103
|
+
if n_failed:
|
|
104
|
+
print(_color(f" Failed: {n_failed}", _RED))
|
|
105
|
+
if n_sim_fail:
|
|
106
|
+
print(_color(f" Simulation errors: {n_sim_fail}", _RED))
|
|
107
|
+
if n_no_ref:
|
|
108
|
+
print(_color(f" No baseline: {n_no_ref}", _YELLOW))
|
|
109
|
+
if n_not_run:
|
|
110
|
+
print(_color(f" {STATUS_LABELS[NOT_RUN]}: {n_not_run}", _GREY))
|
|
111
|
+
if advised:
|
|
112
|
+
print(_color(f" With advisory failures: {len(advised)}", _YELLOW))
|
|
113
|
+
if warned:
|
|
114
|
+
print(_color(f" Structural warnings: {len(warned)}", _YELLOW))
|
|
115
|
+
|
|
116
|
+
# Print failure details
|
|
117
|
+
if failures:
|
|
118
|
+
print()
|
|
119
|
+
print(_color("Failure Details:", _BOLD))
|
|
120
|
+
print("-" * 80)
|
|
121
|
+
for comp in failures:
|
|
122
|
+
id_tag = f"[{comp.test_id}] " if comp.test_id else ""
|
|
123
|
+
print(f"\n {id_tag}{comp.model_id}")
|
|
124
|
+
if comp.error_message:
|
|
125
|
+
print(f" Error: {comp.error_message}")
|
|
126
|
+
for block in failure_blocks(comp):
|
|
127
|
+
for line in block:
|
|
128
|
+
print(line)
|
|
129
|
+
# A group that fails on its own: the test's reason where no
|
|
130
|
+
# criterion failed.
|
|
131
|
+
for line in group_failures(comp):
|
|
132
|
+
print(f" {line}")
|
|
133
|
+
|
|
134
|
+
# The criteria that failed under a `warn` in the tests that passed
|
|
135
|
+
if advised:
|
|
136
|
+
print()
|
|
137
|
+
print(_color("Advisory Failures (under `warn`: they fail no test):", _BOLD))
|
|
138
|
+
print("-" * 80)
|
|
139
|
+
for comp, advisory in advised:
|
|
140
|
+
id_tag = f"[{comp.test_id}] " if comp.test_id else ""
|
|
141
|
+
print(f"\n {id_tag}{comp.model_id}")
|
|
142
|
+
for block in _blocks(advisory):
|
|
143
|
+
for line in block:
|
|
144
|
+
print(line)
|
|
145
|
+
|
|
146
|
+
# Print structural warnings
|
|
147
|
+
if warned:
|
|
148
|
+
print()
|
|
149
|
+
print(_color("Structural Warnings:", _BOLD))
|
|
150
|
+
print("-" * 80)
|
|
151
|
+
for comp in warned:
|
|
152
|
+
print(f"\n {comp.model_id}")
|
|
153
|
+
for w in comp.warnings:
|
|
154
|
+
print(f" {w.text}")
|
|
155
|
+
|
|
156
|
+
return exit_code(comparisons)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def failure_blocks(comp: TestComparison) -> list[list[str]]:
|
|
160
|
+
"""One block of lines per criterion of ``comp`` that did not pass
|
|
161
|
+
(:func:`~.status.failed_criteria`, which JUnit's lines are read from
|
|
162
|
+
too), in TREE order: the line that says which criterion it is, the
|
|
163
|
+
criterion in its metric's own words — or why it was not scored — and
|
|
164
|
+
that metric's detail pairs.
|
|
165
|
+
|
|
166
|
+
The blocks used to be the same five generic numbers under the
|
|
167
|
+
variable's name alone: a start-up window's nrmse and an advisory
|
|
168
|
+
comparison against a soft check read as one variable failing twice,
|
|
169
|
+
with nothing to tell them apart."""
|
|
170
|
+
return _blocks(failed_criteria(comp))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _blocks(criteria: list[FailedCriterion]) -> list[list[str]]:
|
|
174
|
+
blocks: list[list[str]] = []
|
|
175
|
+
for criterion in criteria:
|
|
176
|
+
lines = [f" {criterion.head}"]
|
|
177
|
+
if criterion.reason:
|
|
178
|
+
# A sentinel (config that could not build, no data to compare)
|
|
179
|
+
# has no meaningful numbers — the reason is the whole finding.
|
|
180
|
+
lines.append(f" {'Not scored:':<20}{criterion.reason}")
|
|
181
|
+
else:
|
|
182
|
+
lines.append(f" {criterion.sentence}")
|
|
183
|
+
lines += [
|
|
184
|
+
f" {label + ':':<20}{value}" for label, value in criterion.details
|
|
185
|
+
]
|
|
186
|
+
blocks.append(lines)
|
|
187
|
+
return blocks
|