Skip to content

Commit d3ecbd0

Browse files
authored
Merge pull request #388 from uriahf/fix/browser-summary-report-offline
Fix direct-open browser summary reports
2 parents c90e233 + f89c7b0 commit d3ecbd0

4 files changed

Lines changed: 115 additions & 15 deletions

File tree

‎src/rtichoke/_report_browser.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def write_html(self, path: str | Path) -> Path:
2424
(output.parent / asset).write_bytes(vendor.joinpath(asset).read_bytes())
2525

2626
spec_json = json.dumps(self.spec, separators=(",", ":")).replace("</", "<\\/")
27+
viz_js = vendor.joinpath("rtichoke-viz.js").read_text(encoding="utf-8")
2728
html = f"""<!doctype html>
2829
<html lang="en">
2930
<head>
@@ -36,7 +37,7 @@ def write_html(self, path: str | Path) -> Path:
3637
<div id="rtichoke-report"></div>
3738
<script id="rtichoke-report-spec" type="application/json">{spec_json}</script>
3839
<script type="module">
39-
import {{ renderReport }} from "./rtichoke-viz.js";
40+
{viz_js}
4041
const spec = JSON.parse(
4142
document.querySelector("#rtichoke-report-spec").textContent
4243
);

‎tests/test_calibration_v2.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def test_real_performance_table_roc_calibration_report_uses_shared_renderer(tmp_
273273
tmp_path / "report.html"
274274
)
275275
html = output.read_text(encoding="utf-8")
276+
viz_js = (tmp_path / "rtichoke-viz.js").read_text(encoding="utf-8")
276277

277278
assert [component["id"] for component in report["components"]] == [
278279
"performance-table",
@@ -281,11 +282,9 @@ def test_real_performance_table_roc_calibration_report_uses_shared_renderer(tmp_
281282
]
282283
assert report["components"][2]["spec"] is calibration
283284
assert _embedded_report(html) == report
284-
assert 'import { renderReport } from "./rtichoke-viz.js";' in html
285+
assert 'import { renderReport } from "./rtichoke-viz.js";' not in html
286+
assert viz_js in html
285287
assert "append(renderReport(spec))" in html
286-
assert "renderCalibrationV2" not in html
287-
assert "renderRocV2" not in html
288-
assert "renderPerformanceTable" not in html
289288

290289

291290
def test_duplicate_evaluation_ids_remain_component_local_in_real_report():

‎tests/test_report_browser.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,11 @@ def test_browser_report_uses_real_producers_and_only_shared_render_report(tmp_pa
4949
tmp_path / "report.html"
5050
)
5151
html = output.read_text(encoding="utf-8")
52+
viz_js = (tmp_path / "rtichoke-viz.js").read_text(encoding="utf-8")
5253

53-
assert 'import { renderReport } from "./rtichoke-viz.js";' in html
54+
assert 'import { renderReport } from "./rtichoke-viz.js";' not in html
55+
assert viz_js in html
5456
assert "append(renderReport(spec))" in html
55-
assert "renderRocV2" not in html
56-
assert "renderPerformanceTable" not in html
57-
assert "renderGainsV2" not in html
5857
assert _embedded_report(html) == report
5958
assert [component["id"] for component in report["components"]] == [
6059
"performance-table",

‎tests/test_summary_report_browser.py‎

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import json
2+
import shutil
3+
import subprocess
4+
from contextlib import contextmanager
5+
from functools import partial
6+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
27
from pathlib import Path
3-
from typing import Any, cast
8+
from threading import Thread
9+
from typing import Any, Iterator, cast
410

511
import numpy as np
12+
import pytest
613

714
from rtichoke.summary_report import summary_report as summary_report_module
815
from rtichoke.summary_report.summary_report import create_summary_report
@@ -38,6 +45,70 @@ def _embedded_report(html: str) -> dict[str, Any]:
3845
return cast(dict[str, Any], json.loads(html[start:end]))
3946

4047

48+
def _chrome_executable() -> str:
49+
for candidate in (
50+
"google-chrome",
51+
"google-chrome-stable",
52+
"chromium",
53+
"chromium-browser",
54+
):
55+
executable = shutil.which(candidate)
56+
if executable is not None:
57+
return executable
58+
pytest.skip("headless Chrome/Chromium is not available")
59+
60+
61+
def _dump_dom(url: str) -> subprocess.CompletedProcess[str]:
62+
return subprocess.run(
63+
[
64+
_chrome_executable(),
65+
"--headless=new",
66+
"--no-sandbox",
67+
"--disable-gpu",
68+
"--enable-logging=stderr",
69+
"--log-level=0",
70+
"--dump-dom",
71+
url,
72+
],
73+
check=False,
74+
capture_output=True,
75+
text=True,
76+
timeout=30,
77+
)
78+
79+
80+
def _rendered_report_html(dom: str) -> str:
81+
marker = '<div id="rtichoke-report">'
82+
start = dom.index(marker) + len(marker)
83+
end = dom.index('<script id="rtichoke-report-spec"', start)
84+
return dom[start:end]
85+
86+
87+
@contextmanager
88+
def _serve(directory: Path) -> Iterator[str]:
89+
handler = partial(SimpleHTTPRequestHandler, directory=str(directory))
90+
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
91+
thread = Thread(target=server.serve_forever, daemon=True)
92+
thread.start()
93+
try:
94+
yield f"http://127.0.0.1:{server.server_port}"
95+
finally:
96+
server.shutdown()
97+
thread.join()
98+
server.server_close()
99+
100+
101+
def _assert_report_rendered(browser: subprocess.CompletedProcess[str]) -> None:
102+
assert browser.returncode == 0, browser.stderr
103+
assert "INFO:CONSOLE" not in browser.stderr, browser.stderr
104+
rendered = _rendered_report_html(browser.stdout)
105+
assert "Performance" in rendered, browser.stderr
106+
assert "ROC" in rendered
107+
assert "Calibration" in rendered
108+
assert "<table" in rendered
109+
assert rendered.count("<svg") >= 2
110+
111+
41112
def test_default_summary_report_keeps_historical_r_backend(monkeypatch, capsys):
42113
probs, reals = _inputs()
43114
calls = []
@@ -90,7 +161,8 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
90161

91162
assert result == output
92163
assert output.exists()
93-
assert (tmp_path / "rtichoke-viz.js").exists()
164+
viz_js_path = tmp_path / "rtichoke-viz.js"
165+
assert viz_js_path.exists()
94166
assert (tmp_path / "rtichoke-viz.css").exists()
95167

96168
html = output.read_text(encoding="utf-8")
@@ -107,11 +179,40 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
107179
]
108180
assert report["components"][1]["spec"]["schemaVersion"] == "2.0"
109181
assert report["components"][2]["spec"]["schemaVersion"] == "2.0"
110-
assert 'import { renderReport } from "./rtichoke-viz.js";' in html
182+
assert 'import { renderReport } from "./rtichoke-viz.js";' not in html
183+
assert viz_js_path.read_text(encoding="utf-8") in html
111184
assert "append(renderReport(spec))" in html
112-
assert "renderPerformanceTable" not in html
113-
assert "renderRocV2" not in html
114-
assert "renderCalibrationV2" not in html
185+
186+
187+
def test_browser_summary_report_executes_when_opened_directly(tmp_path):
188+
probs, reals = _inputs()
189+
output = create_summary_report(
190+
probs,
191+
reals,
192+
renderer="browser",
193+
output_file=tmp_path / "browser_report.html",
194+
)
195+
assert isinstance(output, Path)
196+
197+
browser = _dump_dom(output.resolve().as_uri())
198+
199+
_assert_report_rendered(browser)
200+
201+
202+
def test_browser_summary_report_executes_over_localhost(tmp_path):
203+
probs, reals = _inputs()
204+
output = create_summary_report(
205+
probs,
206+
reals,
207+
renderer="browser",
208+
output_file=tmp_path / "browser_report.html",
209+
)
210+
assert isinstance(output, Path)
211+
212+
with _serve(output.parent) as base_url:
213+
browser = _dump_dom(f"{base_url}/{output.name}")
214+
215+
_assert_report_rendered(browser)
115216

116217

117218
def test_browser_summary_report_preserves_component_local_identity(tmp_path):

0 commit comments

Comments
 (0)