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,284 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ {% if auto_refresh %}<meta http-equiv="refresh" content="5">{% endif %}
6
+ <title>{{ title }} — {{ total }} tests</title>
7
+ <style>
8
+ {{ dstf_css|safe }}
9
+ body { font-family: var(--dstf-font); margin: 1.5em; background: var(--dstf-page); color: var(--dstf-text); }
10
+ h1 { margin-bottom: 0.2em; font-size: 1.4em; }
11
+ .status-bar { font-size: 0.85em; color: var(--dstf-text-secondary); margin-bottom: 1em; }
12
+ .counters { display: flex; gap: 0.6em; flex-wrap: wrap; margin-bottom: 1em; }
13
+ /* Sticky top region: h1 + status-bar + progress bar + counter pills stay
14
+ * visible as the table body scrolls. The thead rows below stack at
15
+ * `top: var(--chrome-height)` and `top: var(--chrome-height) + var(--header-row-height)`,
16
+ * with both variables updated by a ResizeObserver in dashboard.js so
17
+ * counter-pill wrapping (or any future chrome content) doesn't desync the offsets. */
18
+ #sticky-chrome {
19
+ position: sticky;
20
+ top: 0;
21
+ z-index: 50;
22
+ background: var(--dstf-page);
23
+ margin-left: -1.5em;
24
+ margin-right: -1.5em;
25
+ padding: 0 1.5em 0.6em;
26
+ border-bottom: 1px solid var(--dstf-border);
27
+ }
28
+ .counter { background: var(--dstf-surface); border: 1px solid var(--dstf-border); border-left-width: 4px; border-radius: 4px; padding: 0.4em 0.8em; min-width: 90px; cursor: pointer; user-select: none; transition: background 0.1s, box-shadow 0.1s; }
29
+ .counter:hover { background: var(--dstf-surface-hover); }
30
+ .counter.disabled { cursor: default; opacity: 0.5; }
31
+ .counter.disabled:hover { background: var(--dstf-surface); }
32
+ .counter.active { box-shadow: 0 0 0 2px var(--dstf-accent) inset; background: var(--dstf-selection); }
33
+ .counter .label { font-size: 0.7em; color: var(--dstf-text-muted); text-transform: uppercase; }
34
+ .counter .value { font-size: 1.3em; font-weight: 600; }
35
+ .counter.queued { border-left-color: var(--dstf-queued-fill); } .counter.queued .value { color: var(--dstf-text-secondary); }
36
+ .counter.running { border-left-color: var(--dstf-running-fill); } .counter.running .value { color: var(--dstf-running-dark); }
37
+ .counter.pass { border-left-color: var(--dstf-pass-fill); } .counter.pass .value { color: var(--dstf-pass-dark); }
38
+ .counter.fail { border-left-color: var(--dstf-fail-fill); } .counter.fail .value { color: var(--dstf-fail-dark); }
39
+ .counter.sim-fail { border-left-color: var(--dstf-sim-fail-fill); } .counter.sim-fail .value { color: var(--dstf-sim-fail-dark); }
40
+ .counter.no-ref { border-left-color: var(--dstf-no-ref-fill); } .counter.no-ref .value { color: var(--dstf-no-ref-dark); }
41
+ .counter.warnings { border-left-color: var(--dstf-warn-fill); } .counter.warnings .value { color: var(--dstf-warn-dark); }
42
+ .counter.timed-out { border-left-color: var(--dstf-timed-out-fill); } .counter.timed-out .value { color: var(--dstf-timed-out-dark); }
43
+ .counter.not-run { border-left-color: var(--dstf-not-run-fill); } .counter.not-run .value { color: var(--dstf-text-secondary); }
44
+ .counter.total { border-left-color: var(--dstf-text-secondary); }
45
+ .bar { height: 14px; background: var(--dstf-border-hairline); border-radius: 7px; overflow: hidden; margin-bottom: 1em; display: flex; }
46
+ .bar span { display: block; height: 100%; }
47
+ .bar .b-pass { background: var(--dstf-pass-fill); }
48
+ .bar .b-fail { background: var(--dstf-fail-fill); }
49
+ .bar .b-timed-out { background: var(--dstf-timed-out-fill); }
50
+ .bar .b-pending { background: var(--dstf-pending-fill); }
51
+ .bar .b-running { background: var(--dstf-running-fill); }
52
+ table { border-collapse: collapse; width: 100%; background: var(--dstf-surface); font-size: 0.85em; }
53
+ th, td { border: 1px solid var(--dstf-border); padding: 4px 8px; text-align: left; }
54
+ th { background: var(--dstf-surface-hover); cursor: pointer; user-select: none; position: relative; }
55
+ /* Drag handle on the right edge of each header th. Drives a JS-driven
56
+ * width override via dashboard.js; widths persist to localStorage so they
57
+ * survive meta-refresh ticks. The handle is a thin invisible strip
58
+ * (visible cursor only) so it doesn't visually clutter the header bar. */
59
+ .col-resize-handle {
60
+ position: absolute; top: 0; right: -3px; bottom: 0;
61
+ width: 6px; cursor: col-resize; z-index: 5;
62
+ user-select: none;
63
+ }
64
+ .col-resize-handle:hover { background: var(--dstf-selection); }
65
+ th.resizing { background: var(--dstf-selection); }
66
+ /* Two thead rows stack below the sticky chrome. Header row sits at the
67
+ * chrome's bottom edge; filter row sits below the header row. CSS variables
68
+ * are kept fresh by the ResizeObserver in dashboard.js (fallback hard-coded
69
+ * sizes are sane defaults if JS is disabled or hasn't run yet). */
70
+ thead tr:first-child th {
71
+ position: sticky;
72
+ top: var(--chrome-height, 140px);
73
+ z-index: 40;
74
+ }
75
+ thead tr.filter-row th {
76
+ position: sticky;
77
+ top: calc(var(--chrome-height, 140px) + var(--header-row-height, 28px));
78
+ z-index: 40;
79
+ }
80
+ th[data-sort]:hover { background: var(--dstf-surface-active); }
81
+ th.sorted-asc::after { content: " ▲"; color: var(--dstf-accent); font-size: 0.85em; }
82
+ th.sorted-desc::after { content: " ▼"; color: var(--dstf-accent); font-size: 0.85em; }
83
+ input.col-filter { box-sizing: border-box; width: 100%; padding: 2px 4px; font-size: 0.78em; border: 1px solid var(--dstf-border); border-radius: 2px; }
84
+ input.col-filter.bad { border-color: var(--dstf-fail-fill); outline: 1px solid var(--dstf-fail-fill); }
85
+ .filter-toggles { display: flex; gap: 0.3em; margin-top: -0.4em; }
86
+ .filter-toggle { font-family: var(--dstf-mono); font-size: 0.8em; padding: 0.1em 0.5em; border: 1px solid var(--dstf-border); border-radius: 3px; background: var(--dstf-surface); cursor: pointer; }
87
+ .filter-toggle[aria-pressed="true"] { background: var(--dstf-selection); border-color: var(--dstf-accent); }
88
+ tr.queued td { color: var(--dstf-text-muted); }
89
+ tr.running td { background: var(--dstf-running-tint); font-weight: 500; }
90
+ tr.pass td { background: var(--dstf-pass-tint); }
91
+ tr.fail td { background: var(--dstf-fail-tint); }
92
+ tr.sim-fail td { background: var(--dstf-fail-tint); }
93
+ tr.no-ref td { background: var(--dstf-no-ref-tint); }
94
+ tr.timed-out td { background: var(--dstf-timed-out-tint); }
95
+ tr.not-run td { background: var(--dstf-not-run-tint); color: var(--dstf-text-muted); }
96
+ tr.pending td { background: var(--dstf-pending-tint); }
97
+ .status-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.75em; font-weight: 700; letter-spacing: 0.03em; color: white; white-space: nowrap; }
98
+ .status-badge.pass { background: var(--dstf-pass-fill); }
99
+ .status-badge.fail { background: var(--dstf-fail-fill); }
100
+ .status-badge.sim-fail { background: var(--dstf-sim-fail-fill); }
101
+ .status-badge.no-ref { background: var(--dstf-no-ref-fill); }
102
+ .status-badge.timed-out { background: var(--dstf-timed-out-fill); }
103
+ .status-badge.queued { background: var(--dstf-queued-fill); }
104
+ .status-badge.not-run { background: var(--dstf-not-run-fill); }
105
+ .status-badge.running { background: var(--dstf-running-fill); }
106
+ .status-badge.pending { background: var(--dstf-pending-fill); }
107
+ .warn { color: var(--dstf-warn-dark); font-weight: 600; }
108
+ .unscored { color: var(--dstf-text-muted); font-style: italic; }
109
+ /* A word after a cell's number that says what kind of number it is
110
+ (Sim (s)'s "cpu"): smaller and muted, the number stays the eye's. */
111
+ .cell-tag { font-size: 0.78em; color: var(--dstf-text-muted); }
112
+ td.tagged { white-space: nowrap; } /* the tag stays on its number's line */
113
+ .detail-cell {
114
+ max-width: var(--col-w-detail, 30em);
115
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
116
+ color: var(--dstf-text-secondary); font-size: 0.8em;
117
+ }
118
+ /* Model column truncation. Default ellipsis at 28em so long fully-qualified
119
+ * IDs don't push the rest of the table off-screen. Users can drag the
120
+ * column-resize handle on any header th to widen any column; for the
121
+ * Model column, dragging updates --col-w-model so the td's max-width
122
+ * follows. Widths persist via localStorage across meta-refresh ticks.
123
+ * The full model ID is always in the td's title= attr for hover. */
124
+ .model-cell {
125
+ max-width: var(--col-w-model, 28em);
126
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
127
+ }
128
+ .model-cell a { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
129
+ .sel-col { width: 28px; text-align: center; cursor: default; }
130
+ .sel-col input { cursor: pointer; }
131
+ table th.sel-col { cursor: default; }
132
+ table th.sel-col:hover { background: var(--dstf-surface-hover); }
133
+ tr.row-selected td { background: var(--dstf-selection) !important; }
134
+ a { color: var(--dstf-accent); text-decoration: none; }
135
+ a:hover { text-decoration: underline; }
136
+
137
+ /* Sticky footer: hidden when 0 selected. */
138
+ .sel-footer {
139
+ position: fixed; bottom: 0; left: 0; right: 0;
140
+ background: var(--dstf-surface); border-top: 2px solid var(--dstf-accent);
141
+ box-shadow: 0 -2px 8px var(--dstf-shadow);
142
+ padding: 0.6em 1.2em; font-size: 0.85em;
143
+ display: none; /* shown by JS when selection.size > 0 */
144
+ align-items: center; gap: 0.8em; flex-wrap: wrap;
145
+ z-index: 10;
146
+ }
147
+ .sel-footer.shown { display: flex; }
148
+ .sel-footer .count { font-weight: 600; color: var(--dstf-accent); }
149
+ .sel-footer .count .hidden-hint { color: var(--dstf-text-muted); font-weight: normal; font-size: 0.9em; margin-left: 0.4em; }
150
+ .sel-footer button { padding: 0.35em 0.8em; border: 1px solid var(--dstf-border); border-radius: 3px; background: var(--dstf-surface); cursor: pointer; font-size: 0.85em; }
151
+ .sel-footer button:hover { background: var(--dstf-surface-hover); }
152
+ .sel-footer button.btn-primary { background: var(--dstf-accent); color: var(--dstf-on-accent); border-color: var(--dstf-accent); }
153
+ .sel-footer button.btn-primary:hover { background: var(--dstf-accent-hover); }
154
+ .sel-footer .cmd-out { flex: 1; min-width: 16em; font-family: var(--dstf-mono); font-size: 0.8em; padding: 0.3em 0.5em; border: 1px solid var(--dstf-border); border-radius: 3px; background: var(--dstf-page); }
155
+ .sel-footer .cmd-kind-label { font-size: 0.85em; color: var(--dstf-text-secondary); display: inline-flex; align-items: center; gap: 0.35em; }
156
+ .sel-footer .cmd-kind-label select { font-family: var(--dstf-mono); font-size: 0.85em; padding: 0.25em 0.3em; border: 1px solid var(--dstf-border); border-radius: 3px; background: var(--dstf-surface); }
157
+ .sel-footer .save-status { color: var(--dstf-pass-fill); font-size: 0.82em; display: none; }
158
+ .sel-footer .save-status.shown { display: inline; }
159
+ .sel-footer .help-link { color: var(--dstf-text-muted); font-size: 0.78em; margin-left: auto; }
160
+ .sel-footer .help-link:hover { color: var(--dstf-accent); }
161
+ /* Pad the bottom of body so the table's last row isn't hidden under the footer when it's open. */
162
+ body.has-selection { padding-bottom: 5em; }
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <div id="sticky-chrome">
167
+ <h1>{{ title }}</h1>
168
+ {% if run_metadata %}
169
+ <div class="run-meta" style="font-size:0.8em; color: var(--dstf-text-muted); margin:-4px 0 8px;">
170
+ <strong>{{ run_metadata.label }}</strong>{% if run_metadata.tool_version and run_metadata.tool_version != run_metadata.label %} <span title="version reported by the backend">({{ run_metadata.tool_version }})</span>{% endif %}
171
+ {% if run_metadata.msl %}· MSL {{ run_metadata.msl }}{% endif %}
172
+ {% if run_metadata.os %}· {{ run_metadata.os }}{% endif %}
173
+ {% if run_metadata.dstf_version %}· DSTF {{ run_metadata.dstf_version }}{% endif %}
174
+ {% if run_metadata.generated_str %}· {{ run_metadata.generated_str }}{% endif %}
175
+ </div>
176
+ {% endif %}
177
+ <div class="status-bar">
178
+ {% if auto_refresh %}Auto-refreshing every 5s ·
179
+ elapsed <span id="header-elapsed">{{ "%.0f"|format(elapsed) }}s</span> ·
180
+ <span id="header-wall">…</span>
181
+ {% elif run_total == 0 %}Final report · {{ total }} test{{ "" if total == 1 else "s" }}{% if updated_at %} · finished
182
+ <span id="header-wall-final">…</span>{% endif %}
183
+ {% elif total > run_total %}Final report · {{ total }} tests · last run: {{ run_total }} test{{ "" if run_total == 1 else "s" }} in {{ "%.1f"|format(elapsed) }}s · finished
184
+ <span id="header-wall-final">…</span>
185
+ {% else %}Final report · {{ total }} test{{ "" if total == 1 else "s" }} · ran for {{ "%.0f"|format(elapsed) }}s · finished
186
+ <span id="header-wall-final">…</span>
187
+ {% endif %}
188
+ </div>
189
+
190
+ <div class="bar" id="progress-bar"></div>
191
+ <div class="counters" id="counters" title="Click a pill to toggle its filter; click Total to clear"></div>
192
+ <div class="filter-toggles">
193
+ <button type="button" class="filter-toggle" id="toggle-regex" aria-pressed="false" title="Regex in every column filter">.*</button>
194
+ <button type="button" class="filter-toggle" id="toggle-case" aria-pressed="false" title="Match case in every column filter">Aa</button>
195
+ </div>
196
+ </div>
197
+
198
+ <table id="results-table">
199
+ <thead>
200
+ <tr>
201
+ <th class="sel-col" title="Select all visible / clear"><input type="checkbox" id="sel-all" onclick="toggleAllVisible(this.checked)"></th>
202
+ {% for c in columns %} <th data-sort="{{ 'num' if c.kind == 'number' else 'text' }}" data-key="{{ c.key }}"{% if c.title %} title="{{ c.title }}"{% endif %}>{{ c.label }}<span class="col-resize-handle"></span></th>
203
+ {% endfor %}</tr>
204
+ <tr class="filter-row">
205
+ <th class="sel-col"></th>
206
+ {#- One typed filter per column (filters.js); the status column has none:
207
+ the pills filter it. #}
208
+ {% for c in columns %} <th>{% if c.kind in filter_hints %}<input class="col-filter" data-col-filter="{{ c.key }}" data-kind="{{ c.kind }}" placeholder="{{ filter_hints[c.kind][0] }}" title="{{ filter_hints[c.kind][1] }}">{% endif %}</th>
209
+ {% endfor %}</tr>
210
+ </thead>
211
+ <tbody id="results-tbody">
212
+ {#- One attribute per column, from reporting.rows: data-<key> is what a
213
+ column filter reads, data-sort-<key> what a sort reads ("" = no value:
214
+ no filter matches it, and it sorts last). data-status stays the status CLASS: the pills', the
215
+ counts' and the live tick's key. #}
216
+ {% for t in tests %}
217
+ <tr class="{{ t.status_class }}"
218
+ data-status="{{ t.status_class }}"
219
+ {% for c in columns %}{% if c.key != 'status' %} data-{{ c.key }}="{{ c.filter_text(t) }}"
220
+ {% endif %} data-sort-{{ c.key }}="{{ c.sort(t) | sortval }}"
221
+ {% endfor %} data-started-wall="{{ t.started_wall or '' }}">
222
+ <td class="sel-col"><input type="checkbox" class="row-sel" onchange="onRowToggle(this)"></td>
223
+ {% for c in columns %}{% set text = c.text(t) %}
224
+ {%- if c.key == 'test' %} <td>{% if t.test_key %}<a href="{{ t.test_key }}/">{{ text }}</a>{% else %}{{ text }}{% endif %}</td>
225
+ {% elif c.key == 'ref' %} <td>{% if t.ref_file %}<a href="{{ t.ref_file }}" title="Open reference JSON">{{ text }}</a>{% else %}{{ text }}{% endif %}</td>
226
+ {% elif c.key == 'model' %} <td class="model-cell" title="{{ t.model_id }}">{% if t.report_page %}<a href="reports/{{ t.report_page }}/interactive.html">{{ text }}</a>{% else %}{{ text }}{% endif %}</td>
227
+ {% elif c.key == 'status' %} <td><span class="status-badge {{ t.status_class }}">{{ text }}</span></td>
228
+ {% elif c.key == 'resolution' %} <td>{% if t.resolution == 'mixed' %}<span title="{% for k, v in t.field_sources.items() %}{{ k }}: {{ v }}{% if not loop.last %}, {% endif %}{% endfor %}">{{ text }}</span>{% elif t.resolution %}<span title="all fields from {{ text }}">{{ text }}</span>{% else %}{{ text }}{% endif %}</td>
229
+ {% elif c.key == 'elapsed' %} <td class="elapsed-cell">{{ text }}</td>
230
+ {% elif c.key == 'nrmse' %} <td>{% if t.worst_score is none and t.n_unscored %}<span class="unscored" title="No nrmse criterion on this test produced a score; {{ t.n_unscored }} of its criteria {{ 'is' if t.n_unscored == 1 else 'are' }} not scored (a metric config that would not build, or no data to compare)">{{ text }}</span>{% else %}{{ text }}{% endif %}</td>
231
+ {% elif c.key == 'warnings' %} <td>{% if t.n_warnings %}<span class="warn">{{ text }}</span>{% else %}{{ text }}{% endif %}</td>
232
+ {% elif c.key == 'detail' %} <td class="detail-cell" title="{{ t.message or '' }}">{{ t.message or '' }}</td>
233
+ {% else %}{% set tag = c.tag(t) if c.tag else none %}{% set tip = c.tip(t) if c.tip else none %} <td{% if tag %} class="tagged"{% endif %}{% if tip %} title="{{ tip }}"{% endif %}>{{ text }}{% if tag %} <span class="cell-tag">{{ tag }}</span>{% endif %}</td>
234
+ {% endif %}{% endfor %}</tr>
235
+ {% endfor %}
236
+ </tbody>
237
+ </table>
238
+
239
+ <div class="sel-footer" id="sel-footer">
240
+ <span class="count">
241
+ <span id="sel-count">0</span> selected<span class="hidden-hint" id="sel-hidden-hint"></span>
242
+ </span>
243
+ <button id="btn-clear-sel" onclick="clearSelection()">Clear</button>
244
+ <button id="btn-download-txt" onclick="downloadFilter()">Download .txt</button>
245
+ <label class="cmd-kind-label">Command
246
+ <select id="cmd-kind" onchange="syncCommand()" title="Which dstf command to build for the selection">
247
+ <option value="run">run</option>
248
+ <option value="compare">compare</option>
249
+ <option value="compare --accept">compare --accept</option>
250
+ <option value="run --rerun failed,sim-failed">run --rerun failed,sim-failed</option>
251
+ <option value="author">author</option>
252
+ </select>
253
+ </label>
254
+ <button class="btn-primary" id="btn-copy-cmd" onclick="copyCommand()">Copy rerun command</button>
255
+ <span class="save-status" id="save-status">Copied!</span>
256
+ <input class="cmd-out" id="cmd-out" readonly placeholder="Selection produces a rerun command here…">
257
+ <a class="help-link" href="#" onclick="event.preventDefault();alert('Pick the command with the Command menu. Plain run also gets --merge (keep prior results on disk) and --report (regenerate a full HTML report covering all tests, not just the filtered subset); drop --merge to focus the report on only the rerun tests. compare re-scores the last run without simulating, and compare --accept stores those results as the new baselines. author opens the authoring tool on the selection.')" title="What does each command do?">?</a>
258
+ </div>
259
+
260
+ <script>
261
+ const DASHBOARD_MODE = '{{ mode }}';
262
+ const DASHBOARD_TOTAL = {{ total }};
263
+ const RERUN_PREFIX = {{ rerun_prefix | tojson }};
264
+ // Identity of THIS report (work dir + run start). The persisted UI state
265
+ // is keyed by location.pathname, which successive runs into the same
266
+ // work_dir share — the selection is scoped to this key so one report's
267
+ // selection can never leak into the next one's rerun command.
268
+ const REPORT_KEY = {{ report_key | tojson }};
269
+ // Wall-clock anchors for the live-tick header + per-row elapsed.
270
+ // SNAPSHOT_WALL = epoch seconds when the server wrote this snapshot.
271
+ // SNAPSHOT_ELAPSED = run elapsed at that moment.
272
+ // Live elapsed = SNAPSHOT_ELAPSED + (Date.now()/1000 - SNAPSHOT_WALL),
273
+ // which keeps ticking between meta-refresh tick that doesn't itself
274
+ // fire on every UI update (the server only re-snapshots on test
275
+ // state changes, so quiet stretches would otherwise look frozen).
276
+ const SNAPSHOT_WALL = {{ updated_at }};
277
+ const SNAPSHOT_ELAPSED = {{ elapsed }};
278
+ // The count pills, in PILL_STATUSES' order: [status class, the word a person reads].
279
+ const STATUSES = {{ statuses | tojson }};
280
+ {% include 'filters.js' %}
281
+ {% include 'dashboard.js' %}
282
+ </script>
283
+ </body>
284
+ </html>