Skip to content

Commit e741b66

Browse files
committed
feat: show test code in visualize_tests results page
1 parent d92c25e commit e741b66

1 file changed

Lines changed: 154 additions & 1 deletion

File tree

tools/visualize_tests.py

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,34 @@
1111
from pathlib import Path
1212
from typing import Iterator
1313

14+
from _testcode import get_test_code
15+
1416
IMAGE_DIR = Path("tests/result_images").resolve()
1517
EXPECTED_SUFFIX = "-expected"
1618
FAILED_SUFFIX = "-failed-diff"
1719
RESULT_PREFIX = "tests/result_images"
1820
BASELINE_PREFIX = "tests/baseline_images"
21+
TESTS_DIR = IMAGE_DIR.parent
22+
23+
try:
24+
from pygments import highlight
25+
from pygments.formatters import HtmlFormatter
26+
from pygments.lexers import PythonLexer
27+
28+
def highlight_python(source: str) -> str:
29+
"""
30+
Python source as HTML spans with pygments token classes
31+
"""
32+
return highlight(source, PythonLexer(), HtmlFormatter(nowrap=True))
33+
34+
except ImportError:
35+
from html import escape
36+
37+
def highlight_python(source: str) -> str:
38+
"""
39+
Python source as plain escaped text (pygments unavailable)
40+
"""
41+
return escape(source)
1942

2043

2144
@dataclass(frozen=True)
@@ -98,6 +121,12 @@ def get_test_images() -> Iterator[TestImage]:
98121
--amber-soft: #fcf3d4;
99122
--link: #2c7be5;
100123
--img-bg: #ffffff;
124+
--code-kw: #cf222e;
125+
--code-str: #0a3069;
126+
--code-num: #0550ae;
127+
--code-com: #6e7781;
128+
--code-fn: #8250df;
129+
--code-builtin: #953800;
101130
}
102131
103132
@media (prefers-color-scheme: dark) {
@@ -119,6 +148,12 @@ def get_test_images() -> Iterator[TestImage]:
119148
--amber-soft: #3a2c10;
120149
--link: #6cb6ff;
121150
--img-bg: #ffffff;
151+
--code-kw: #f47067;
152+
--code-str: #96d0ff;
153+
--code-num: #6cb6ff;
154+
--code-com: #768390;
155+
--code-fn: #dcbdfb;
156+
--code-builtin: #f69d50;
122157
}
123158
}
124159
@@ -140,6 +175,12 @@ def get_test_images() -> Iterator[TestImage]:
140175
--amber-soft: #3a2c10;
141176
--link: #6cb6ff;
142177
--img-bg: #ffffff;
178+
--code-kw: #f47067;
179+
--code-str: #96d0ff;
180+
--code-num: #6cb6ff;
181+
--code-com: #768390;
182+
--code-fn: #dcbdfb;
183+
--code-builtin: #f69d50;
143184
}
144185
145186
* { box-sizing: border-box; }
@@ -505,6 +546,55 @@ def get_test_images() -> Iterator[TestImage]:
505546
border-radius: 4px;
506547
}
507548
549+
.code-toggle.active {
550+
background: var(--chip-active-bg);
551+
color: var(--chip-active-fg);
552+
border-color: var(--chip-active-bg);
553+
}
554+
555+
.code-panel {
556+
display: flex;
557+
flex-direction: column;
558+
gap: 4px;
559+
}
560+
561+
.code-panel.hidden { display: none; }
562+
563+
.code-path {
564+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco,
565+
Consolas, monospace;
566+
font-size: 12px;
567+
align-self: flex-start;
568+
}
569+
570+
pre.code {
571+
margin: 0;
572+
padding: 10px 12px;
573+
border: 1px solid var(--border);
574+
border-radius: 6px;
575+
background: var(--chip-bg);
576+
overflow-x: auto;
577+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco,
578+
Consolas, monospace;
579+
font-size: 12px;
580+
line-height: 1.5;
581+
}
582+
583+
pre.code .k, pre.code .kn, pre.code .ow, pre.code .o {
584+
color: var(--code-kw);
585+
}
586+
pre.code .s, pre.code .s1, pre.code .s2, pre.code .sd,
587+
pre.code .sa, pre.code .si {
588+
color: var(--code-str);
589+
}
590+
pre.code .mi, pre.code .mf { color: var(--code-num); }
591+
pre.code .c1, pre.code .cm {
592+
color: var(--code-com);
593+
font-style: italic;
594+
}
595+
pre.code .nf, pre.code .nc, pre.code .fm { color: var(--code-fn); }
596+
pre.code .nb, pre.code .bp { color: var(--code-builtin); }
597+
508598
.empty {
509599
padding: 40px;
510600
text-align: center;
@@ -711,7 +801,9 @@ def get_test_images() -> Iterator[TestImage]:
711801
for (const row of rows) {
712802
if (row.dataset.status !== 'failed') continue;
713803
714-
const buttons = row.querySelectorAll('.view-buttons button');
804+
const buttons = row.querySelectorAll(
805+
'.view-buttons button[data-view]'
806+
);
715807
for (const btn of buttons) {
716808
btn.addEventListener('click', () => {
717809
const view = btn.dataset.view;
@@ -799,6 +891,17 @@ def get_test_images() -> Iterator[TestImage]:
799891
});
800892
applyTheme();
801893
894+
// Per-row test-code panel toggles.
895+
for (const btn of document.querySelectorAll('button.code-toggle')) {
896+
btn.addEventListener('click', () => {
897+
const row = btn.closest('.test-row');
898+
const panel = row && row.querySelector('.code-panel');
899+
if (!panel) return;
900+
const open = !panel.classList.toggle('hidden');
901+
btn.classList.toggle('active', open);
902+
});
903+
}
904+
802905
// Lightbox: click an anchor-wrapped image to inspect it in-page.
803906
const lightbox = document.getElementById('lightbox');
804907
const lbImg = lightbox.querySelector('img');
@@ -1084,6 +1187,55 @@ def _passed_content(test: TestImage) -> str:
10841187
)
10851188

10861189

1190+
def _code_parts(test: TestImage) -> tuple[str, str]:
1191+
"""
1192+
(toggle button, hidden panel) for a test's code, or empty strings
1193+
"""
1194+
test_file = TESTS_DIR / f"{test.subdir}.py"
1195+
snippet = get_test_code(test_file, test.name)
1196+
if snippet is None:
1197+
return "", ""
1198+
button = '<button class="code-toggle" type="button">{ } Code</button>'
1199+
panel = (
1200+
'<div class="code-panel hidden">'
1201+
f'<a class="code-path" href="../{test.subdir}.py">'
1202+
f"tests/{test.subdir}.py:{snippet.lineno}</a>"
1203+
f'<pre class="code">{highlight_python(snippet.source)}</pre>'
1204+
"</div>"
1205+
)
1206+
return button, panel
1207+
1208+
1209+
def _with_code(content: str, button: str, panel: str) -> str:
1210+
"""
1211+
Content with the code button in its tab strip and the panel under it
1212+
1213+
The panel opens between the strip and the images. Rows without a
1214+
tab strip (passed/new, and failed without an expected image) get
1215+
one holding just the code button.
1216+
"""
1217+
if not button:
1218+
return content
1219+
if '<div class="view-buttons">' in content:
1220+
# The strip's last button (Flip) closes the strip; the code
1221+
# button goes after it.
1222+
content = content.replace(
1223+
"</button></div>",
1224+
f"</button>{button}</div>",
1225+
1,
1226+
)
1227+
else:
1228+
content = content.replace(
1229+
'<div class="content">',
1230+
f'<div class="content"><div class="view-buttons">{button}</div>',
1231+
1,
1232+
)
1233+
# The strip holds only buttons, so its first </div> closes it.
1234+
strip_start = content.find('<div class="view-buttons">')
1235+
strip_end = content.find("</div>", strip_start) + len("</div>")
1236+
return content[:strip_end] + panel + content[strip_end:]
1237+
1238+
10871239
def render_row(test: TestImage) -> str:
10881240
"""
10891241
Build the HTML for a single test row
@@ -1105,6 +1257,7 @@ def render_row(test: TestImage) -> str:
11051257
content = _new_content(test)
11061258
else:
11071259
content = _passed_content(test)
1260+
content = _with_code(content, *_code_parts(test))
11081261

11091262
classes = ["test-row"]
11101263
if test.status == "failed":

0 commit comments

Comments
 (0)