diff --git a/scripts/sitemeasurements.py b/scripts/sitemeasurements.py index af3cd6fef..e6dde4558 100644 --- a/scripts/sitemeasurements.py +++ b/scripts/sitemeasurements.py @@ -729,7 +729,7 @@ def short_title(item: dict) -> str: word = f"issue {issue[1]}" if issue else CHECK_NAMES.get(word, TERMS.get(word, word)) if word not in axis: axis.append(word) - head = ", ".join(part for part in (f"`{thing}`" if thing else "", game) if part) + head = ", ".join(part for part in (thing, game) if part) title = f"{head} ({' '.join(axis)})" if axis and head else head or " ".join(axis) return title or str(item.get("subject") or "") @@ -883,11 +883,28 @@ def _attr(value: object) -> str: def _cell(value: object) -> str: - """Record text placed in raw HTML: escaped, code spans kept, a semicolon - joining two clauses ending a sentence as on the rest of the site.""" + """Record text placed in raw HTML: escaped as text, never read as + Markdown (a run's output is no markup), braces made inert for MDX. A + semicolon joining two clauses ends a sentence, as on the rest of the + site.""" text = " ".join(str(value or "").split()) text = re.sub(r";\s+(\w)", lambda m: ". " + m[1].upper(), text) - return markup.inline_html(text).translate(_BRACES) + return html.escape(text, quote=True).translate(_BRACES) + + +_SAFE_HREF = re.compile(r"^(?:https?://[^\s\"'<>]+|/[^\s\"'<>]*|\.{0,2}/?[\w.~-][^\s\"'<>:]*)$") + + +def _href(url: object) -> str | None: + """A link target from a record: http(s) or relative only, else none.""" + text = str(url or "") + return _attr(text) if text and _SAFE_HREF.match(text) and not text.lower().startswith( + ("javascript:", "data:", "vbscript:")) else None + + +def _link(url: object, text: str) -> str: + target = _href(url) + return f'{text}' if target else text def _dots(item: dict) -> str: @@ -902,7 +919,7 @@ def _dots(item: dict) -> str: def _chip(item: dict) -> str: words, kind = RESULT_WORDS.get(item["status"], (item["status"], "muted")) - return f'{html.escape(words)}' + return f'{_cell(words)}' def _sentence_html(label: str, value: object, markup_ready: bool = False) -> str: @@ -913,7 +930,7 @@ def _sentence_html(label: str, value: object, markup_ready: bool = False) -> str def _details(item: dict) -> list[str]: core = item["core"] - build = _cell(core["library"]) + (f" at {html.escape(core['commit'][:12])}" + build = _cell(core["library"]) + (f" at {_cell(core['commit'][:12])}" if core.get("commit") else "") reps = f"{item['variants']} variant{'s' if item['variants'] != 1 else ''}, " \ f"{item['repetitions']} repetition{'s' if item['repetitions'] != 1 else ''} each" @@ -927,7 +944,7 @@ def _details(item: dict) -> list[str]: _sentence_html("Proves", ", ".join(item.get("proves") or [])), _sentence_html("Ran on", item.get("host_short") or item["host"]), _sentence_html("Core build", build and f"{build}. {reps}", markup_ready=True), - f'

Full record{runs}.

', + f'

{_link(item["record_url"], "Full record")}{runs}.

', ] return [line for line in lines if line] @@ -948,7 +965,7 @@ def _row(item: dict, title: str, page_host: str, main_core: str) -> list[str]: elsewhere = (item.get("host_short") or item["host"]) != page_host title = _cell(title) if core_label(item) != main_core: - title += f' {html.escape(core_label(item))}' + title += f' {_cell(core_label(item))}' return [ '
', ("" @@ -974,6 +991,13 @@ def by_file(items: list[dict]) -> list[tuple[str, list[dict]]]: return sorted(groups.items(), key=lambda kv: kv[0] == "") +def folded(item: dict) -> bool: + """A row the block folds away: a test from before the boot oracle, or a + run that recorded reference frames for the oracle, which a reviewer + reads and a visitor does not.""" + return item["status"] == "no verdict" or str(item.get("subject") or "").startswith("references-") + + def summary_line(items: list[dict], page_host: str) -> str: """N tests, the confirmed and open counts when there are any, the last run and its computer.""" @@ -1005,15 +1029,15 @@ def tested_section(name: str, items: list[dict]) -> list[str]: f'SQLite') ordered = sorted(items, key=lambda i: (i["date"], i["subject"], core_label(i)), reverse=True) benches = [i for i in ordered if i["kind"] == "bench"] - earlier = [i for i in ordered if i["kind"] != "bench" and i["status"] == "no verdict"] - current = [i for i in ordered if i["kind"] != "bench" and i["status"] != "no verdict"] + earlier = [i for i in ordered if i["kind"] != "bench" and folded(i)] + current = [i for i in ordered if i["kind"] != "bench" and not folded(i)] lines = ["## Tested", "", '
', - f'

{html.escape(summary_line(items, page_host))} {links}

'] + f'

{_cell(summary_line(items, page_host))} {links}

'] if current or earlier: lines.append('") for file, group in by_file(current): - label = f"{html.escape(file)}" if file else "No file varied" + label = f"{_cell(file)}" if file else "No file varied" lines.append(f'

{label}

') for item in group: lines += _row(item, titles[item["id"]], page_host, main_core) @@ -1022,9 +1046,12 @@ def tested_section(name: str, items: list[dict]) -> list[str]: 'the same colour for the same result. ' " No file staged.

") if earlier: + old = sum(1 for i in earlier if i["status"] == "no verdict") + parts = [f"{old} test{'s' if old != 1 else ''} from before the boot oracle" if old else "", + (f"{len(earlier) - old} reference run{'s' if len(earlier) - old != 1 else ''}" + if len(earlier) - old else "")] lines += ['
', - f"{len(earlier)} earlier test{'s' if len(earlier) != 1 else ''}, " - "before the boot oracle"] + f"{_cell(' and '.join(p for p in parts if p))}"] for item in earlier: file = Path(str(item.get("variable") or "")).name title = f"{file}, {titles[item['id']]}" if file else titles[item["id"]] @@ -1038,9 +1065,9 @@ def tested_section(name: str, items: list[dict]) -> list[str]: timing = item.get("timing") or {} title = _cell(titles[item["id"]]) if core_label(item) != main_core: - title += f' {html.escape(core_label(item))}' + title += f' {_cell(core_label(item))}' table.append( - f'{title}' + f'{_link(item["record_url"], title)}' f'{timing.get("ms_per_frame", 0):.3f}{timing.get("fps", 0):.1f}' f'{timing.get("stdev", 0):.3f}{item["repetitions"]}') lines.append("".join([*table, ""])) diff --git a/tests/test_measurements.py b/tests/test_measurements.py index 890884fc7..294aeb5a1 100644 --- a/tests/test_measurements.py +++ b/tests/test_measurements.py @@ -458,17 +458,17 @@ class SitePages(unittest.TestCase): cases = [ ({"subject": "mpeg-card-lunar-cr2-build", "variable": "mpeg_card.bin", "content": "Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00"}, - "`mpeg_card.bin`, Lunar (CR2 build)"), + "mpeg_card.bin, Lunar (CR2 build)"), ({"subject": "negative-scph5501-bin-mednafen-psx-bust-a-move-2-arcade-edition-usa", "variable": "scph5501.bin", "content": "Bust-A-Move 2 - Arcade Edition (USA)", "core": {"file": "mednafen_psx_libretro.so"}}, - "`scph5501.bin`, Bust-A-Move 2 (wrong files)"), + "scph5501.bin, Bust-A-Move 2 (wrong files)"), ({"subject": "issue85-lunar-opening-movie", "variable": None, "content": "Redump: Lunar - Silver Star Story Complete (Japan), T-27904G V1.00"}, "Lunar (issue 85 opening movie)"), ({"subject": "with-command-auth-device-and-command-get-9a211715", "variable": "mpeg_card.bin", "content": "Redump: Lunar"}, - "`mpeg_card.bin`, Lunar (with command auth device and get)"), + "mpeg_card.bin, Lunar (with command auth device and get)"), ] for item, title in cases: self.assertEqual(sm.short_title(item), title) @@ -498,6 +498,29 @@ class SitePages(unittest.TestCase): # Alone in its file, a test is named by its game. self.assertEqual(sm.distinct_titles(items[:1]), {"a": "Lunar"}) + def test_run_output_is_text_never_markup(self) -> None: + nasty = "`code` {expr} [x](javascript:alert(1)) \"q\" 'q' **b**" + item = self.item( + claim=nasty, method=nasty, statement=nasty, host=nasty, host_short=nasty, content=nasty, + variable=f"{nasty}.bin", subject="s", title=nasty, proves=[nasty], runs=2, flags=1, + core={"library": nasty, "commit": "{``}", "file": f"{nasty}_libretro.so"}, + groups=[{"variant": nasty, "group": 0, "result": nasty, "absent": False}], + record_url="javascript:alert(1)") + bench = self.item(id="b", kind="bench", status="measured", subject="bench", title=nasty, + content=nasty, record_url="java\nscript:x", + timing={"ms_per_frame": 1.0, "fps": 1.0, "stdev": 0.0}) + earlier = self.item(id="e", status="no verdict", subject="e", content=nasty, claim=nasty) + text = "\n".join(sm.tested_section("alpha", [item, bench, earlier])) + self.assertNotIn("b", text) + self.assertNotIn("code", text) + self.assertNotRegex(text, r'="[^"]*"q"') + self.assertIn("<script>", text) + self.assertIn("{expr}", text) + def test_no_empty_sentence(self) -> None: text = "\n".join(sm.tested_section("beetle_saturn", [self.item(claim=None, method=None, statement="", kind="run", status="ran")]))