rosetta-sql 1.0.0__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.
- benchmark/generate_csv_data.py +83 -0
- benchmark/import_data.py +168 -0
- rosetta/__init__.py +3 -0
- rosetta/__main__.py +8 -0
- rosetta/benchmark.py +1678 -0
- rosetta/buglist.py +108 -0
- rosetta/cli/__init__.py +11 -0
- rosetta/cli/config_cmd.py +243 -0
- rosetta/cli/exec.py +219 -0
- rosetta/cli/interactive_cmd.py +124 -0
- rosetta/cli/list_cmd.py +215 -0
- rosetta/cli/main.py +617 -0
- rosetta/cli/output.py +545 -0
- rosetta/cli/result.py +61 -0
- rosetta/cli/result_cmd.py +247 -0
- rosetta/cli/run.py +625 -0
- rosetta/cli/status.py +161 -0
- rosetta/comparator.py +205 -0
- rosetta/config.py +139 -0
- rosetta/executor.py +403 -0
- rosetta/flamegraph.py +630 -0
- rosetta/interactive.py +1790 -0
- rosetta/models.py +197 -0
- rosetta/parser.py +308 -0
- rosetta/reporter/__init__.py +1 -0
- rosetta/reporter/bench_html.py +1457 -0
- rosetta/reporter/bench_text.py +162 -0
- rosetta/reporter/history.py +1686 -0
- rosetta/reporter/html.py +644 -0
- rosetta/reporter/text.py +110 -0
- rosetta/runner.py +3089 -0
- rosetta/ui.py +736 -0
- rosetta/whitelist.py +161 -0
- rosetta_sql-1.0.0.dist-info/LICENSE +21 -0
- rosetta_sql-1.0.0.dist-info/METADATA +379 -0
- rosetta_sql-1.0.0.dist-info/RECORD +42 -0
- rosetta_sql-1.0.0.dist-info/WHEEL +5 -0
- rosetta_sql-1.0.0.dist-info/entry_points.txt +2 -0
- rosetta_sql-1.0.0.dist-info/top_level.txt +4 -0
- skills/rosetta/scripts/install_rosetta.py +469 -0
- skills/rosetta/scripts/rosetta_wrapper.py +377 -0
- tests/test_cli.py +749 -0
rosetta/reporter/html.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"""HTML report generator for Rosetta.
|
|
2
|
+
|
|
3
|
+
Generates a single self-contained HTML file with:
|
|
4
|
+
- Dashboard: summary table, pass-rate bars
|
|
5
|
+
- Diff details: side-by-side view with syntax highlighting
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import html
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
from ..models import CompareResult
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger("rosetta")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _escape(text: str) -> str:
|
|
20
|
+
"""HTML-escape a string."""
|
|
21
|
+
return html.escape(text, quote=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_summary_data(comparisons: Dict[str, CompareResult]) -> List[dict]:
|
|
25
|
+
"""Build summary data for the template."""
|
|
26
|
+
rows = []
|
|
27
|
+
for key, cmp in comparisons.items():
|
|
28
|
+
rows.append({
|
|
29
|
+
"key": key,
|
|
30
|
+
"dbms_a": cmp.dbms_a,
|
|
31
|
+
"dbms_b": cmp.dbms_b,
|
|
32
|
+
"matched": cmp.matched,
|
|
33
|
+
"mismatched": cmp.mismatched,
|
|
34
|
+
"whitelisted": cmp.whitelisted,
|
|
35
|
+
"bug_marked": cmp.bug_marked,
|
|
36
|
+
"skipped": cmp.skipped,
|
|
37
|
+
"total": cmp.total_stmts,
|
|
38
|
+
"pass_rate": round(cmp.pass_rate, 1),
|
|
39
|
+
})
|
|
40
|
+
return rows
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_diff_data(comparisons: Dict[str, CompareResult]) -> List[dict]:
|
|
44
|
+
"""Build diff data for the template."""
|
|
45
|
+
sections = []
|
|
46
|
+
for key, cmp in comparisons.items():
|
|
47
|
+
if not cmp.diffs:
|
|
48
|
+
continue
|
|
49
|
+
diffs = []
|
|
50
|
+
for d in cmp.diffs:
|
|
51
|
+
diffs.append({
|
|
52
|
+
"block": d["block"],
|
|
53
|
+
"stmt": d["stmt"][:200],
|
|
54
|
+
"lines_a": d.get("lines_a", []),
|
|
55
|
+
"lines_b": d.get("lines_b", []),
|
|
56
|
+
"context_before": d.get("context_before", []),
|
|
57
|
+
"context_after": d.get("context_after", []),
|
|
58
|
+
"fingerprint": d.get("fingerprint", ""),
|
|
59
|
+
"whitelisted": d.get("whitelisted", False),
|
|
60
|
+
"bug_marked": d.get("bug_marked", False),
|
|
61
|
+
})
|
|
62
|
+
sections.append({
|
|
63
|
+
"key": key,
|
|
64
|
+
"dbms_a": cmp.dbms_a,
|
|
65
|
+
"dbms_b": cmp.dbms_b,
|
|
66
|
+
"diffs": diffs,
|
|
67
|
+
})
|
|
68
|
+
return sections
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8">
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
+
<title>Rosetta Report — {{TEST_NAME}}</title>
|
|
77
|
+
<style>
|
|
78
|
+
:root {
|
|
79
|
+
--bg: #0d1117; --bg2: #161b22; --bg3: #21262d;
|
|
80
|
+
--fg: #c9d1d9; --fg2: #8b949e;
|
|
81
|
+
--green: #3fb950; --green-bg: #12261e;
|
|
82
|
+
--red: #f85149; --red-bg: #2d1315;
|
|
83
|
+
--blue: #58a6ff; --yellow: #d29922;
|
|
84
|
+
--orange: #db8b0b; --orange-bg: #2d2009;
|
|
85
|
+
--purple: #a371f7; --purple-bg: #1e163b;
|
|
86
|
+
--border: #30363d; --accent: #1f6feb;
|
|
87
|
+
}
|
|
88
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
89
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
90
|
+
background: var(--bg); color: var(--fg); line-height: 1.5; padding: 20px; }
|
|
91
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
92
|
+
h1 { color: var(--fg); margin-bottom: 4px; font-size: 24px; }
|
|
93
|
+
.meta { color: var(--fg2); font-size: 14px; margin-bottom: 24px; }
|
|
94
|
+
.meta span { margin-right: 16px; }
|
|
95
|
+
|
|
96
|
+
/* Summary table */
|
|
97
|
+
.summary-card { background: var(--bg2); border: 1px solid var(--border);
|
|
98
|
+
border-radius: 8px; padding: 20px; margin-bottom: 24px; }
|
|
99
|
+
.summary-card h2 { font-size: 18px; margin-bottom: 12px; }
|
|
100
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
101
|
+
th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border);
|
|
102
|
+
color: var(--fg2); font-weight: 600; }
|
|
103
|
+
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
104
|
+
tr:hover { background: var(--bg3); }
|
|
105
|
+
.pass-bar { display: inline-block; height: 8px; border-radius: 4px;
|
|
106
|
+
background: var(--green); vertical-align: middle; }
|
|
107
|
+
.fail-bar { display: inline-block; height: 8px; border-radius: 4px;
|
|
108
|
+
background: var(--red); vertical-align: middle; }
|
|
109
|
+
.bar-bg { display: inline-block; width: 120px; height: 8px; border-radius: 4px;
|
|
110
|
+
background: var(--bg3); vertical-align: middle; position: relative; overflow: hidden; }
|
|
111
|
+
.bar-fill { height: 100%; border-radius: 4px; position: absolute; left: 0; top: 0; }
|
|
112
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px;
|
|
113
|
+
font-size: 12px; font-weight: 600; }
|
|
114
|
+
.badge-pass { background: var(--green-bg); color: var(--green); }
|
|
115
|
+
.badge-fail { background: var(--red-bg); color: var(--red); }
|
|
116
|
+
.num-mismatch { color: var(--red); font-weight: 600; }
|
|
117
|
+
.num-match { color: var(--green); }
|
|
118
|
+
.num-wl { color: var(--orange); }
|
|
119
|
+
.num-bug { color: var(--red); }
|
|
120
|
+
|
|
121
|
+
/* Filter bar */
|
|
122
|
+
.filter-bar { display: flex; gap: 12px; align-items: center;
|
|
123
|
+
margin-bottom: 16px; flex-wrap: wrap; }
|
|
124
|
+
.filter-bar input { background: var(--bg2); border: 1px solid var(--border);
|
|
125
|
+
border-radius: 6px; padding: 6px 12px; color: var(--fg); font-size: 14px;
|
|
126
|
+
width: 300px; outline: none; }
|
|
127
|
+
.filter-bar input:focus { border-color: var(--accent); }
|
|
128
|
+
.filter-bar select { background: var(--bg2); border: 1px solid var(--border);
|
|
129
|
+
border-radius: 6px; padding: 6px 12px; color: var(--fg); font-size: 14px;
|
|
130
|
+
outline: none; }
|
|
131
|
+
|
|
132
|
+
/* Diff sections */
|
|
133
|
+
.diff-section { background: var(--bg2); border: 1px solid var(--border);
|
|
134
|
+
border-radius: 8px; margin-bottom: 16px; overflow: hidden; }
|
|
135
|
+
.diff-section.whitelisted { opacity: 0.55; }
|
|
136
|
+
.diff-section.bug-marked { border-left: 3px solid var(--red); }
|
|
137
|
+
.diff-header { padding: 12px 16px; cursor: pointer; display: flex;
|
|
138
|
+
align-items: center; gap: 12px; user-select: none; }
|
|
139
|
+
.diff-header:hover { background: var(--bg3); }
|
|
140
|
+
.diff-header .arrow { transition: transform 0.2s; color: var(--fg2); }
|
|
141
|
+
.diff-header.open .arrow { transform: rotate(90deg); }
|
|
142
|
+
.diff-header .block-num { color: var(--fg2); font-size: 13px; min-width: 80px; }
|
|
143
|
+
.diff-header .sql-preview { font-family: 'SF Mono', Consolas, monospace;
|
|
144
|
+
font-size: 13px; color: var(--blue); overflow: hidden; text-overflow: ellipsis;
|
|
145
|
+
white-space: nowrap; flex: 1; }
|
|
146
|
+
.diff-body { display: block; border-top: 1px solid var(--border); }
|
|
147
|
+
.diff-body.collapsed { display: none; }
|
|
148
|
+
|
|
149
|
+
/* Whitelist badge on diff header */
|
|
150
|
+
.wl-badge { display: inline-block; padding: 2px 8px; border-radius: 12px;
|
|
151
|
+
font-size: 11px; font-weight: 600; background: var(--orange-bg);
|
|
152
|
+
color: var(--orange); white-space: nowrap; }
|
|
153
|
+
|
|
154
|
+
/* Bug badge on diff header */
|
|
155
|
+
.bug-badge { display: inline-block; padding: 2px 8px; border-radius: 12px;
|
|
156
|
+
font-size: 11px; font-weight: 600; background: var(--red-bg);
|
|
157
|
+
color: var(--red); white-space: nowrap; }
|
|
158
|
+
|
|
159
|
+
/* Whitelist button in diff body */
|
|
160
|
+
.wl-bar { padding: 8px 16px; display: flex; align-items: center; gap: 12px;
|
|
161
|
+
border-bottom: 1px solid var(--border); background: var(--bg); }
|
|
162
|
+
.btn-wl { padding: 4px 14px; border-radius: 6px; font-size: 12px;
|
|
163
|
+
font-weight: 600; cursor: pointer; border: 1px solid var(--border);
|
|
164
|
+
transition: all 0.15s; min-width: 160px; height: 32px; line-height: 22px;
|
|
165
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
166
|
+
box-sizing: border-box; }
|
|
167
|
+
.btn-wl-add { background: var(--orange-bg); color: var(--orange); border-color: var(--orange); }
|
|
168
|
+
.btn-wl-add:hover { opacity: 0.85; }
|
|
169
|
+
.btn-wl-remove { background: var(--bg3); color: var(--fg2); }
|
|
170
|
+
.btn-wl-remove:hover { color: var(--red); border-color: var(--red); }
|
|
171
|
+
.btn-bug-add { background: var(--red-bg); color: var(--red); border-color: var(--red); }
|
|
172
|
+
.btn-bug-add:hover { opacity: 0.85; }
|
|
173
|
+
.btn-bug-remove { background: var(--bg3); color: var(--fg2); }
|
|
174
|
+
.btn-bug-remove:hover { color: var(--red); border-color: var(--red); }
|
|
175
|
+
.wl-status { font-size: 12px; color: var(--fg2); }
|
|
176
|
+
|
|
177
|
+
/* Side-by-side diff */
|
|
178
|
+
.side-by-side { display: grid; grid-template-columns: 1fr 1fr; }
|
|
179
|
+
.diff-pane { overflow-x: auto; }
|
|
180
|
+
.diff-pane-header { padding: 8px 12px; font-size: 13px; font-weight: 600;
|
|
181
|
+
color: var(--fg2); background: var(--bg3); border-bottom: 1px solid var(--border); }
|
|
182
|
+
.diff-pane:first-child { border-right: 1px solid var(--border); }
|
|
183
|
+
.diff-line { font-family: 'SF Mono', Consolas, monospace; font-size: 13px;
|
|
184
|
+
padding: 1px 12px; white-space: pre-wrap; word-break: break-all;
|
|
185
|
+
min-height: 22px; line-height: 22px; }
|
|
186
|
+
.diff-line.added { background: var(--green-bg); }
|
|
187
|
+
.diff-line.removed { background: var(--red-bg); }
|
|
188
|
+
.diff-line.context { }
|
|
189
|
+
.diff-line.empty { color: var(--bg3); }
|
|
190
|
+
|
|
191
|
+
/* Comparison tab */
|
|
192
|
+
.comp-tabs { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
193
|
+
.comp-tab { padding: 6px 16px; border-radius: 6px; cursor: pointer;
|
|
194
|
+
font-size: 14px; border: 1px solid var(--border); background: var(--bg2);
|
|
195
|
+
color: var(--fg2); }
|
|
196
|
+
.comp-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
197
|
+
.comp-tab .tab-count { font-size: 12px; margin-left: 4px; }
|
|
198
|
+
|
|
199
|
+
.no-diff { padding: 40px; text-align: center; color: var(--fg2); font-size: 16px; }
|
|
200
|
+
|
|
201
|
+
/* Context lines (surrounding blocks) */
|
|
202
|
+
.context-bar { padding: 6px 16px; font-size: 12px; color: var(--fg2);
|
|
203
|
+
background: var(--bg); border-bottom: 1px solid var(--border);
|
|
204
|
+
font-family: 'SF Mono', Consolas, monospace; line-height: 1.6; }
|
|
205
|
+
.context-bar .ctx-label { color: var(--fg2); font-weight: 600;
|
|
206
|
+
margin-right: 6px; font-size: 11px; text-transform: uppercase; }
|
|
207
|
+
.context-bar .ctx-item { display: block; padding: 1px 0; }
|
|
208
|
+
.context-bar .ctx-block { color: var(--yellow); margin-right: 4px; }
|
|
209
|
+
.context-bar .ctx-sql { color: var(--fg2); }
|
|
210
|
+
.context-bar .ctx-current { color: var(--red); font-weight: 600; }
|
|
211
|
+
|
|
212
|
+
/* Toast notification */
|
|
213
|
+
.toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px;
|
|
214
|
+
border-radius: 8px; font-size: 14px; color: #fff; z-index: 9999;
|
|
215
|
+
transition: opacity 0.3s; pointer-events: none; }
|
|
216
|
+
.toast-success { background: var(--green); }
|
|
217
|
+
.toast-error { background: var(--red); }
|
|
218
|
+
|
|
219
|
+
/* Responsive */
|
|
220
|
+
@media (max-width: 900px) {
|
|
221
|
+
.side-by-side { grid-template-columns: 1fr; }
|
|
222
|
+
.diff-pane:first-child { border-right: none; border-bottom: 1px solid var(--border); }
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<div class="container">
|
|
228
|
+
<div style="display:flex;align-items:center;gap:16px;margin-bottom:4px">
|
|
229
|
+
<h1>Rosetta Report</h1>
|
|
230
|
+
<a href="../index.html" style="color:var(--blue);font-size:14px;text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px">◀ History</a>
|
|
231
|
+
<a href="../playground.html" style="color:var(--green);font-size:14px;text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px">▶ Playground</a>
|
|
232
|
+
<a href="../whitelist.html" style="color:var(--orange);font-size:14px;text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px">☶ Whitelist</a>
|
|
233
|
+
<a href="../buglist.html" style="color:var(--red);font-size:14px;text-decoration:none;border:1px solid var(--border);border-radius:6px;padding:4px 12px">🐛 Buglist</a>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="meta">
|
|
236
|
+
<span>Test: <strong>{{TEST_NAME}}</strong></span>
|
|
237
|
+
<span>Time: {{TIME}}</span>
|
|
238
|
+
<span>Baseline: <strong>{{BASELINE}}</strong></span>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div class="summary-card">
|
|
242
|
+
<h2>Summary</h2>
|
|
243
|
+
<table>
|
|
244
|
+
<thead>
|
|
245
|
+
<tr>
|
|
246
|
+
<th>Comparison</th><th>Status</th><th>Match</th><th>Mismatch</th>
|
|
247
|
+
<th>Whitelist</th><th>Bug</th><th>Skip</th><th>Total</th><th>Pass Rate</th>
|
|
248
|
+
</tr>
|
|
249
|
+
</thead>
|
|
250
|
+
<tbody id="summary-body"></tbody>
|
|
251
|
+
</table>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div id="diff-container">
|
|
255
|
+
<div class="comp-tabs" id="comp-tabs"></div>
|
|
256
|
+
<div class="filter-bar">
|
|
257
|
+
<input type="text" id="search-input" placeholder="Search SQL statements...">
|
|
258
|
+
<select id="wl-filter">
|
|
259
|
+
<option value="all">All diffs</option>
|
|
260
|
+
<option value="active" selected>Non-whitelisted only</option>
|
|
261
|
+
<option value="whitelisted">Whitelisted only</option>
|
|
262
|
+
<option value="bug">Bug-marked only</option>
|
|
263
|
+
<option value="unmarked">Unmarked only</option>
|
|
264
|
+
</select>
|
|
265
|
+
</div>
|
|
266
|
+
<div id="diff-list"></div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div id="toast" class="toast" style="opacity:0"></div>
|
|
271
|
+
|
|
272
|
+
<script>
|
|
273
|
+
const SUMMARY = {{SUMMARY_JSON}};
|
|
274
|
+
const DIFFS = {{DIFFS_JSON}};
|
|
275
|
+
|
|
276
|
+
function showToast(msg, type) {
|
|
277
|
+
const t = document.getElementById('toast');
|
|
278
|
+
t.textContent = msg;
|
|
279
|
+
t.className = 'toast toast-' + (type || 'success');
|
|
280
|
+
t.style.opacity = '1';
|
|
281
|
+
setTimeout(() => { t.style.opacity = '0'; }, 2500);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function callWhitelistAPI(action, body) {
|
|
285
|
+
const port = location.port || '80';
|
|
286
|
+
const base = location.protocol + '//' + location.hostname + ':' + port;
|
|
287
|
+
return fetch(base + '/api/whitelist/' + action, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {'Content-Type': 'application/json'},
|
|
290
|
+
body: JSON.stringify(body),
|
|
291
|
+
}).then(r => r.json());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function callBuglistAPI(action, body) {
|
|
295
|
+
const port = location.port || '80';
|
|
296
|
+
const base = location.protocol + '//' + location.hostname + ':' + port;
|
|
297
|
+
return fetch(base + '/api/buglist/' + action, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {'Content-Type': 'application/json'},
|
|
300
|
+
body: JSON.stringify(body),
|
|
301
|
+
}).then(r => r.json());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Render summary table
|
|
305
|
+
const tbody = document.getElementById('summary-body');
|
|
306
|
+
SUMMARY.forEach(r => {
|
|
307
|
+
const effectiveMismatch = r.mismatched - (r.whitelisted || 0);
|
|
308
|
+
const status = effectiveMismatch <= 0;
|
|
309
|
+
const pct = r.pass_rate;
|
|
310
|
+
const row = document.createElement('tr');
|
|
311
|
+
row.innerHTML = `
|
|
312
|
+
<td>${esc(r.key)}</td>
|
|
313
|
+
<td><span class="badge ${status ? 'badge-pass' : 'badge-fail'}">${status ? 'PASS' : 'FAIL'}</span></td>
|
|
314
|
+
<td class="num-match">${r.matched}</td>
|
|
315
|
+
<td class="${effectiveMismatch > 0 ? 'num-mismatch' : ''}">${effectiveMismatch > 0 ? effectiveMismatch : 0}</td>
|
|
316
|
+
<td class="num-wl">${r.whitelisted || 0}</td>
|
|
317
|
+
<td class="num-bug">${r.bug_marked || 0}</td>
|
|
318
|
+
<td>${r.skipped}</td>
|
|
319
|
+
<td>${r.total}</td>
|
|
320
|
+
<td>
|
|
321
|
+
<span class="bar-bg"><span class="bar-fill" style="width:${pct}%;background:${pct>=100?'var(--green)':pct>=90?'var(--yellow)':'var(--red)'}"></span></span>
|
|
322
|
+
${pct}%
|
|
323
|
+
</td>
|
|
324
|
+
`;
|
|
325
|
+
tbody.appendChild(row);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Render comparison tabs
|
|
329
|
+
const tabsEl = document.getElementById('comp-tabs');
|
|
330
|
+
const listEl = document.getElementById('diff-list');
|
|
331
|
+
let activeTab = DIFFS.length > 0 ? DIFFS[0].key : null;
|
|
332
|
+
|
|
333
|
+
function renderTabs() {
|
|
334
|
+
tabsEl.innerHTML = '';
|
|
335
|
+
if (DIFFS.length === 0) {
|
|
336
|
+
listEl.innerHTML = '<div class="no-diff">All results matched — no differences found.</div>';
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
DIFFS.forEach(sec => {
|
|
340
|
+
const active = sec.diffs.filter(d => !d.whitelisted).length;
|
|
341
|
+
const wl = sec.diffs.filter(d => d.whitelisted).length;
|
|
342
|
+
const bugs = sec.diffs.filter(d => d.bug_marked).length;
|
|
343
|
+
const tab = document.createElement('div');
|
|
344
|
+
tab.className = 'comp-tab' + (sec.key === activeTab ? ' active' : '');
|
|
345
|
+
let label = `${esc(sec.key)}<span class="tab-count">(${active}`;
|
|
346
|
+
if (wl > 0) label += ` +${wl} wl`;
|
|
347
|
+
if (bugs > 0) label += ` +${bugs} bug`;
|
|
348
|
+
label += ')</span>';
|
|
349
|
+
tab.innerHTML = label;
|
|
350
|
+
tab.onclick = () => { activeTab = sec.key; renderTabs(); renderDiffs(); };
|
|
351
|
+
tabsEl.appendChild(tab);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function esc(s) {
|
|
356
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''').replace(/`/g,'`').replace(/\$\{/g,'${');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function buildDiffBody(d, sec) {
|
|
360
|
+
const normA = d.lines_a.map(l => l.trim());
|
|
361
|
+
const normB = d.lines_b.map(l => l.trim());
|
|
362
|
+
const matchedA = new Set();
|
|
363
|
+
const matchedB = new Set();
|
|
364
|
+
let bStart = 0;
|
|
365
|
+
for (let ai = 0; ai < normA.length; ai++) {
|
|
366
|
+
for (let bi = bStart; bi < normB.length; bi++) {
|
|
367
|
+
if (normA[ai] === normB[bi] && !matchedB.has(bi)) {
|
|
368
|
+
matchedA.add(ai); matchedB.add(bi); bStart = bi + 1; break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const left = []; const right = [];
|
|
373
|
+
d.lines_a.forEach((l, i) => {
|
|
374
|
+
left.push(`<div class="diff-line ${matchedA.has(i)?'context':'removed'}">${esc(l)}</div>`);
|
|
375
|
+
});
|
|
376
|
+
d.lines_b.forEach((l, i) => {
|
|
377
|
+
right.push(`<div class="diff-line ${matchedB.has(i)?'context':'added'}">${esc(l)}</div>`);
|
|
378
|
+
});
|
|
379
|
+
return `<div class="side-by-side"><div class="diff-pane"><div class="diff-pane-header">${esc(sec.dbms_a)} (baseline)</div>${left.join('')}</div><div class="diff-pane"><div class="diff-pane-header">${esc(sec.dbms_b)}</div>${right.join('')}</div></div>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function renderDiffs() {
|
|
383
|
+
listEl.innerHTML = '';
|
|
384
|
+
const sec = DIFFS.find(s => s.key === activeTab);
|
|
385
|
+
if (!sec) return;
|
|
386
|
+
const query = document.getElementById('search-input').value.toLowerCase();
|
|
387
|
+
const wlFilter = document.getElementById('wl-filter').value;
|
|
388
|
+
|
|
389
|
+
const frag = document.createDocumentFragment();
|
|
390
|
+
sec.diffs.forEach(d => {
|
|
391
|
+
if (query && !d.stmt.toLowerCase().includes(query)) return;
|
|
392
|
+
if (wlFilter === 'active' && d.whitelisted) return;
|
|
393
|
+
if (wlFilter === 'whitelisted' && !d.whitelisted) return;
|
|
394
|
+
if (wlFilter === 'bug' && !d.bug_marked) return;
|
|
395
|
+
if (wlFilter === 'unmarked' && (d.whitelisted || d.bug_marked)) return;
|
|
396
|
+
|
|
397
|
+
const section = document.createElement('div');
|
|
398
|
+
let sectionCls = 'diff-section';
|
|
399
|
+
if (d.whitelisted) sectionCls += ' whitelisted';
|
|
400
|
+
if (d.bug_marked) sectionCls += ' bug-marked';
|
|
401
|
+
section.className = sectionCls;
|
|
402
|
+
|
|
403
|
+
const header = document.createElement('div');
|
|
404
|
+
header.className = 'diff-header' + (d.whitelisted ? '' : ' open');
|
|
405
|
+
const wlTag = d.whitelisted ? '<span class="wl-badge">whitelisted</span>' : '';
|
|
406
|
+
const bugTag = d.bug_marked ? '<span class="bug-badge">bug</span>' : '';
|
|
407
|
+
header.innerHTML = `<span class="arrow">▶</span><span class="block-num">Block ${d.block}</span>${wlTag}${bugTag}<span class="sql-preview">${esc(d.stmt)}</span>`;
|
|
408
|
+
|
|
409
|
+
const body = document.createElement('div');
|
|
410
|
+
body.className = 'diff-body' + (d.whitelisted ? ' collapsed' : '');
|
|
411
|
+
|
|
412
|
+
// Whitelist action bar
|
|
413
|
+
const wlBar = document.createElement('div');
|
|
414
|
+
wlBar.className = 'wl-bar';
|
|
415
|
+
if (d.whitelisted) {
|
|
416
|
+
const span = document.createElement('span');
|
|
417
|
+
span.className = 'wl-status';
|
|
418
|
+
span.textContent = '\u2713 This diff is whitelisted';
|
|
419
|
+
wlBar.appendChild(span);
|
|
420
|
+
const btn = document.createElement('button');
|
|
421
|
+
btn.className = 'btn-wl btn-wl-remove';
|
|
422
|
+
btn.textContent = 'Remove from whitelist';
|
|
423
|
+
btn.addEventListener('click', () => removeFromWL(btn, d.fingerprint));
|
|
424
|
+
wlBar.appendChild(btn);
|
|
425
|
+
} else {
|
|
426
|
+
const btn = document.createElement('button');
|
|
427
|
+
btn.className = 'btn-wl btn-wl-add';
|
|
428
|
+
btn.textContent = '+ Add to whitelist';
|
|
429
|
+
btn.addEventListener('click', () => addToWL(btn, d.fingerprint, sec.dbms_a, sec.dbms_b, d.block, d.stmt.substring(0,200)));
|
|
430
|
+
wlBar.appendChild(btn);
|
|
431
|
+
}
|
|
432
|
+
// Bug mark buttons
|
|
433
|
+
if (d.bug_marked) {
|
|
434
|
+
const span = document.createElement('span');
|
|
435
|
+
span.className = 'wl-status';
|
|
436
|
+
span.style.color = 'var(--red)';
|
|
437
|
+
span.textContent = '\u2713 Marked as bug';
|
|
438
|
+
wlBar.appendChild(span);
|
|
439
|
+
const btn = document.createElement('button');
|
|
440
|
+
btn.className = 'btn-wl btn-bug-remove';
|
|
441
|
+
btn.textContent = 'Unmark bug';
|
|
442
|
+
btn.addEventListener('click', () => removeFromBug(btn, d.fingerprint));
|
|
443
|
+
wlBar.appendChild(btn);
|
|
444
|
+
} else {
|
|
445
|
+
const btn = document.createElement('button');
|
|
446
|
+
btn.className = 'btn-wl btn-bug-add';
|
|
447
|
+
btn.textContent = '\uD83D\uDC1B Mark as bug';
|
|
448
|
+
btn.addEventListener('click', () => addToBug(btn, d.fingerprint, sec.dbms_a, sec.dbms_b, d.block, d.stmt.substring(0,200)));
|
|
449
|
+
wlBar.appendChild(btn);
|
|
450
|
+
}
|
|
451
|
+
const fpSpan = document.createElement('span');
|
|
452
|
+
fpSpan.className = 'wl-status';
|
|
453
|
+
fpSpan.style.cssText = 'color:var(--fg2);font-size:12px';
|
|
454
|
+
fpSpan.textContent = 'fp: ' + d.fingerprint.substring(0,8) + '\u2026';
|
|
455
|
+
wlBar.appendChild(fpSpan);
|
|
456
|
+
body.appendChild(wlBar);
|
|
457
|
+
|
|
458
|
+
// Render context bar
|
|
459
|
+
const ctxBefore = d.context_before || [];
|
|
460
|
+
const ctxAfter = d.context_after || [];
|
|
461
|
+
if (ctxBefore.length > 0 || ctxAfter.length > 0) {
|
|
462
|
+
const ctxBar = document.createElement('div');
|
|
463
|
+
ctxBar.className = 'context-bar';
|
|
464
|
+
let ctxHtml = '';
|
|
465
|
+
ctxBefore.forEach(c => {
|
|
466
|
+
ctxHtml += `<span class="ctx-item"><span class="ctx-block">Block ${c.block}</span><span class="ctx-sql">${esc(c.stmt)}</span></span>`;
|
|
467
|
+
});
|
|
468
|
+
ctxHtml += `<span class="ctx-item"><span class="ctx-block">Block ${d.block}</span><span class="ctx-current">▶ ${esc(d.stmt)}</span></span>`;
|
|
469
|
+
ctxAfter.forEach(c => {
|
|
470
|
+
ctxHtml += `<span class="ctx-item"><span class="ctx-block">Block ${c.block}</span><span class="ctx-sql">${esc(c.stmt)}</span></span>`;
|
|
471
|
+
});
|
|
472
|
+
ctxBar.innerHTML = ctxHtml;
|
|
473
|
+
body.appendChild(ctxBar);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const diffContent = document.createElement('div');
|
|
477
|
+
diffContent.innerHTML = buildDiffBody(d, sec);
|
|
478
|
+
body.appendChild(diffContent);
|
|
479
|
+
|
|
480
|
+
header.onclick = () => {
|
|
481
|
+
header.classList.toggle('open');
|
|
482
|
+
body.classList.toggle('collapsed');
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
section.appendChild(header);
|
|
486
|
+
section.appendChild(body);
|
|
487
|
+
frag.appendChild(section);
|
|
488
|
+
});
|
|
489
|
+
listEl.appendChild(frag);
|
|
490
|
+
|
|
491
|
+
if (listEl.children.length === 0) {
|
|
492
|
+
listEl.innerHTML = '<div class="no-diff">No differences match the current filter.</div>';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function addToWL(btn, fp, dbmsA, dbmsB, block, stmt) {
|
|
497
|
+
btn.disabled = true;
|
|
498
|
+
btn.textContent = 'Adding...';
|
|
499
|
+
callWhitelistAPI('add', {fingerprint: fp, dbms_a: dbmsA, dbms_b: dbmsB, block: block, stmt: stmt})
|
|
500
|
+
.then(r => {
|
|
501
|
+
if (r.ok) {
|
|
502
|
+
// Update local data
|
|
503
|
+
DIFFS.forEach(sec => { sec.diffs.forEach(d => { if (d.fingerprint === fp) d.whitelisted = true; }); });
|
|
504
|
+
renderTabs(); renderDiffs();
|
|
505
|
+
showToast('Added to whitelist', 'success');
|
|
506
|
+
} else {
|
|
507
|
+
showToast('Failed: ' + (r.error || 'unknown'), 'error');
|
|
508
|
+
btn.disabled = false; btn.textContent = '+ Add to whitelist';
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
.catch(e => {
|
|
512
|
+
showToast('API error: ' + e.message, 'error');
|
|
513
|
+
btn.disabled = false; btn.textContent = '+ Add to whitelist';
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function removeFromWL(btn, fp) {
|
|
518
|
+
btn.disabled = true;
|
|
519
|
+
btn.textContent = 'Removing...';
|
|
520
|
+
callWhitelistAPI('remove', {fingerprint: fp})
|
|
521
|
+
.then(r => {
|
|
522
|
+
if (r.ok) {
|
|
523
|
+
DIFFS.forEach(sec => { sec.diffs.forEach(d => { if (d.fingerprint === fp) d.whitelisted = false; }); });
|
|
524
|
+
renderTabs(); renderDiffs();
|
|
525
|
+
showToast('Removed from whitelist', 'success');
|
|
526
|
+
} else {
|
|
527
|
+
showToast('Failed: ' + (r.error || 'unknown'), 'error');
|
|
528
|
+
btn.disabled = false; btn.textContent = 'Remove from whitelist';
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
.catch(e => {
|
|
532
|
+
showToast('API error: ' + e.message, 'error');
|
|
533
|
+
btn.disabled = false; btn.textContent = 'Remove from whitelist';
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function addToBug(btn, fp, dbmsA, dbmsB, block, stmt) {
|
|
538
|
+
btn.disabled = true;
|
|
539
|
+
btn.textContent = 'Marking...';
|
|
540
|
+
callBuglistAPI('add', {fingerprint: fp, dbms_a: dbmsA, dbms_b: dbmsB, block: block, stmt: stmt})
|
|
541
|
+
.then(r => {
|
|
542
|
+
if (r.ok) {
|
|
543
|
+
DIFFS.forEach(sec => { sec.diffs.forEach(d => { if (d.fingerprint === fp) d.bug_marked = true; }); });
|
|
544
|
+
renderTabs(); renderDiffs();
|
|
545
|
+
showToast('Marked as bug', 'success');
|
|
546
|
+
} else {
|
|
547
|
+
showToast('Failed: ' + (r.error || 'unknown'), 'error');
|
|
548
|
+
btn.disabled = false; btn.textContent = '\uD83D\uDC1B Mark as bug';
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
.catch(e => {
|
|
552
|
+
showToast('API error: ' + e.message, 'error');
|
|
553
|
+
btn.disabled = false; btn.textContent = '\uD83D\uDC1B Mark as bug';
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function removeFromBug(btn, fp) {
|
|
558
|
+
btn.disabled = true;
|
|
559
|
+
btn.textContent = 'Unmarking...';
|
|
560
|
+
callBuglistAPI('remove', {fingerprint: fp})
|
|
561
|
+
.then(r => {
|
|
562
|
+
if (r.ok) {
|
|
563
|
+
DIFFS.forEach(sec => { sec.diffs.forEach(d => { if (d.fingerprint === fp) d.bug_marked = false; }); });
|
|
564
|
+
renderTabs(); renderDiffs();
|
|
565
|
+
showToast('Bug mark removed', 'success');
|
|
566
|
+
} else {
|
|
567
|
+
showToast('Failed: ' + (r.error || 'unknown'), 'error');
|
|
568
|
+
btn.disabled = false; btn.textContent = 'Unmark bug';
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
.catch(e => {
|
|
572
|
+
showToast('API error: ' + e.message, 'error');
|
|
573
|
+
btn.disabled = false; btn.textContent = 'Unmark bug';
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
document.getElementById('search-input').addEventListener('input', renderDiffs);
|
|
578
|
+
document.getElementById('wl-filter').addEventListener('change', renderDiffs);
|
|
579
|
+
|
|
580
|
+
// Sync whitelisted/bug_marked state from API on page load.
|
|
581
|
+
// The static HTML embeds a snapshot; if the user later added/removed entries
|
|
582
|
+
// via the API, we need to refresh the flags before rendering.
|
|
583
|
+
function syncFromAPI() {
|
|
584
|
+
const wlReq = callWhitelistAPI('list', {}).then(r => (r && r.ok) ? r.entries || {} : null).catch(() => null);
|
|
585
|
+
const blReq = callBuglistAPI('list', {}).then(r => (r && r.ok) ? r.entries || {} : null).catch(() => null);
|
|
586
|
+
Promise.all([wlReq, blReq]).then(([wlData, blData]) => {
|
|
587
|
+
if (wlData === null && blData === null) {
|
|
588
|
+
// API unavailable (e.g. viewing static file) — keep embedded data as-is
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
let changed = false;
|
|
592
|
+
DIFFS.forEach(sec => {
|
|
593
|
+
sec.diffs.forEach(d => {
|
|
594
|
+
const newWl = wlData ? !!wlData[d.fingerprint] : d.whitelisted;
|
|
595
|
+
const newBug = blData ? !!blData[d.fingerprint] : d.bug_marked;
|
|
596
|
+
if (d.whitelisted !== newWl || d.bug_marked !== newBug) {
|
|
597
|
+
d.whitelisted = newWl;
|
|
598
|
+
d.bug_marked = newBug;
|
|
599
|
+
changed = true;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
if (changed) { renderTabs(); renderDiffs(); }
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
renderTabs();
|
|
608
|
+
renderDiffs();
|
|
609
|
+
syncFromAPI();
|
|
610
|
+
</script>
|
|
611
|
+
</body>
|
|
612
|
+
</html>"""
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def write_html_report(path: str, test_file: str,
|
|
616
|
+
comparisons: Dict[str, CompareResult],
|
|
617
|
+
baseline: str = ""):
|
|
618
|
+
"""Generate a self-contained HTML report file."""
|
|
619
|
+
summary = _build_summary_data(comparisons)
|
|
620
|
+
diffs = _build_diff_data(comparisons)
|
|
621
|
+
|
|
622
|
+
test_name = test_file.rsplit("/", 1)[-1] if "/" in test_file else test_file
|
|
623
|
+
|
|
624
|
+
page = _HTML_TEMPLATE
|
|
625
|
+
page = page.replace("{{TEST_NAME}}", _escape(test_name))
|
|
626
|
+
page = page.replace("{{TIME}}",
|
|
627
|
+
_escape(time.strftime("%Y-%m-%d %H:%M:%S")))
|
|
628
|
+
page = page.replace("{{BASELINE}}", _escape(baseline or "N/A"))
|
|
629
|
+
# Safely embed JSON in <script>: escape '</' to prevent breaking
|
|
630
|
+
# the script tag, and escape backslash sequences that might confuse
|
|
631
|
+
# the JS parser.
|
|
632
|
+
def _safe_json(obj):
|
|
633
|
+
s = json.dumps(obj, ensure_ascii=False)
|
|
634
|
+
# Prevent "</script>" or any "</" from closing the script element
|
|
635
|
+
s = s.replace("<", "\\u003c")
|
|
636
|
+
return s
|
|
637
|
+
|
|
638
|
+
page = page.replace("{{SUMMARY_JSON}}", _safe_json(summary))
|
|
639
|
+
page = page.replace("{{DIFFS_JSON}}", _safe_json(diffs))
|
|
640
|
+
|
|
641
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
642
|
+
f.write(page)
|
|
643
|
+
|
|
644
|
+
log.info("HTML report written: %s", path)
|