From e7d0c0183c92da2359b4203b76d873950c03c082 Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Tue, 27 Oct 2026 19:40:16 +0100
Subject: [PATCH] feat: show each test as one row with its proof
The Tested block opens on one summary line, then a row per test: a short title, a result chip, a dot per variant and the date. Method, claim, computer, core build and the record wait in the row's details. Tests from before the boot oracle fold under one line, benchmarks get their own table. beetle_saturn shows under 120 words before anything is opened.
---
scripts/sitemeasurements.py | 310 +++++++++++++++++++++++++++----
tests/test_measurements.py | 97 +++++++++-
website/src/styles/retrobios.css | 75 ++++++++
wiki/measurements.md | 35 ++--
4 files changed, 449 insertions(+), 68 deletions(-)
diff --git a/scripts/sitemeasurements.py b/scripts/sitemeasurements.py
index cc35ef362..4d065b631 100644
--- a/scripts/sitemeasurements.py
+++ b/scripts/sitemeasurements.py
@@ -18,6 +18,7 @@ its line at the last commit that touched measurements/.
from __future__ import annotations
+import html
import json
import os
import re
@@ -146,9 +147,20 @@ def index_item(entry: mr.Stored, runs: int, source: dict, profiles: dict,
"runs": runs,
"record_url": permalink(source["repo"], source["commit"], mr.records_path(entry.emulator), entry.line),
"sqlite": f"downloads/measurements/{entry.emulator}.sqlite",
+ "host_short": short_host(record["host"]),
+ "groups": variant_groups(record),
+ "proves": proves(record["id"], profile),
+ "timing": ({"ms_per_frame": record["timing"]["ms_per_frame"]["median"],
+ "fps": record["timing"]["fps"]["median"],
+ "stdev": record["timing"]["ms_per_frame"]["stdev"]}
+ if record.get("timing") else None),
}
+def with_title(item: dict) -> dict:
+ return {**item, "title": short_title(item)}
+
+
def build_index(stored: list[mr.Stored], source: dict | None, profiles: dict,
system_names: dict[str, str] | None = None) -> dict:
"""One item per (emulator, subject): its latest record not superseded."""
@@ -161,7 +173,8 @@ def build_index(stored: list[mr.Stored], source: dict | None, profiles: dict,
for key in sorted(by_subject):
entries = by_subject[key]
latest = max(entries, key=lambda s: (s.record["recorded_at"], s.line))
- items.append(index_item(latest, len(entries), source or {}, profiles, system_names or {}))
+ items.append(with_title(index_item(latest, len(entries), source or {}, profiles,
+ system_names or {})))
items.sort(key=lambda item: (item["emulator"], item["date"], item["subject"]))
return {
"schema_version": INDEX_SCHEMA_VERSION,
@@ -514,9 +527,14 @@ def build(public: Path, root: Path = ROOT, emulators_dir: Path = ROOT / "emulato
# --------------------------------------------------------------------------
def by_emulator(index: dict) -> dict[str, list[dict]]:
+ """Each emulator's items, with the count of its findings still open."""
+ opened: dict[str, int] = {}
+ for line in (index.get("campaign") or {}).get("open") or ():
+ opened[line["emulator"]] = opened.get(line["emulator"], 0) + 1
out: dict[str, list[dict]] = {}
for item in index.get("items", []):
- out.setdefault(item["emulator"], []).append(item)
+ out.setdefault(item["emulator"], []).append(
+ {**item, "open_findings": opened.get(item["emulator"], 0)})
return out
@@ -532,52 +550,262 @@ def _short(commit: str | None) -> str:
return f"`{commit[:12]}`" if commit else "unknown"
-def tested_item(item: dict) -> list[str]:
- """One test: what ran, the result, the method and host beside it, the record."""
- title = item["claim"] or f"Benchmark of {item['content'] or 'the core alone'}"
- runs = "1 run" if item["runs"] == 1 else f"{item['runs']} runs, the latest shown"
- core = item["core"]
- build = f"{_text(core['library'])} at {_short(core['commit'])}" if core.get("commit") \
- else _text(core["library"])
- lines = [f"- **{_text(title)}**", ""]
- if len(item.get("verdicts") or ()) > 1:
- lines += [f" **Result:** `{item['status']}`.", ""]
- lines += [f" - {_text(v['variant'])}: `{v['verdict']}`. {_text(v['detail'])}."
- for v in item["verdicts"]]
- lines.append("")
+# A verdict or a comparison, as the block says it: words and a chip kind.
+RESULT_WORDS = {
+ "boots": ("works", "success"),
+ "bios_error": ("BIOS error", "danger"),
+ "crash": ("crash", "danger"),
+ "hang": ("hang", "warning"),
+ "unknown": ("unknown", "muted"),
+ "mixed": ("differs", "info"),
+ "same": ("no difference", "muted"),
+ "differs": ("differs", "info"),
+ "inconclusive": ("inconclusive", "warning"),
+ "no verdict": ("no verdict", "muted"),
+ "ran": ("ran", "muted"),
+}
+# Words a subject slug spells that read as one term.
+TERMS = {"bios": "BIOS", "vcd": "Video CD", "cd": "CD", "jp": "JP", "us": "US", "eu": "EU",
+ "cr2": "CR2", "mpeg": "MPEG", "psx": "PSX", "hle": "HLE", "usa": "USA"}
+# The campaign's own check names, as the block says them.
+CHECK_NAMES = {"files": "full set", "none": "no BIOS", "negative": "wrong files",
+ "references": "reference run", "variants": "copies", "alias": "other name",
+ "path": "other path", "bench": "benchmark"}
+TITLE_AXIS_WORDS = 4
+
+
+def _words(text: str) -> list[str]:
+ return re.findall(r"[a-z0-9]+", str(text or "").lower())
+
+
+def short_content(label: str | None) -> str:
+ """A content label cut to the game's name: no source prefix, no
+ edition, region or identity detail."""
+ text = str(label or "").strip()
+ if ":" in text.split("(")[0]:
+ text = text.split(":", 1)[1].strip()
+ for cut in (" - ", " (", ", "):
+ text = text.split(cut, 1)[0]
+ return text.strip()
+
+
+def short_title(item: dict) -> str:
+ """A test's name in a few words: the file it varies, the game, and what
+ the subject adds to them (a build, a region, a check of the campaign)."""
+ thing = Path(str(item.get("variable") or "")).name
+ game = short_content(item.get("content"))
+ known = set(_words(thing)) | set(_words(item.get("content"))) | set(_words(game))
+ known |= set(_words(Path(str((item.get("core") or {}).get("file") or "")).name.split("_libretro")[0]))
+ axis = []
+ for word in _words(item.get("subject")):
+ if word in known or re.fullmatch(r"[0-9a-f]{8}", word):
+ continue
+ issue = re.fullmatch(r"issue(\d+)", word)
+ word = f"issue {issue[1]}" if issue else CHECK_NAMES.get(word, TERMS.get(word, word))
+ if word not in axis:
+ axis.append(word)
+ if len(axis) > TITLE_AXIS_WORDS:
+ axis = [*axis[:TITLE_AXIS_WORDS], "..."]
+ head = ", ".join(part for part in (f"`{thing}`" if thing else "", 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 "")
+
+
+def short_host(host: dict) -> str:
+ """The computer as a reader names it: the CPU model and the OS."""
+ cpu = str(host.get("cpu_model") or "unknown CPU")
+ cpu = re.sub(r"\(R\)|\(TM\)|\bCPU\b|@.*$|^\d+(?:st|nd|rd|th) Gen ", "", cpu)
+ cpu = " ".join(cpu.split())
+ system = str(host.get("os") or "")
+ name = "Linux" if "Linux" in system else system.split("-")[0] or "unknown OS"
+ if "microsoft" in system.lower():
+ name += " (WSL2)"
+ return f"{cpu}, {name}"
+
+
+def variant_groups(record: dict) -> list[dict]:
+ """One entry per variant: the result group it falls in, and whether it
+ staged no file. A run that carries verdicts is grouped by verdict, a
+ comparison by the value its observable took."""
+ check = record.get("check") or {}
+ position: dict[str, int] = {}
+ result: dict[str, str] = {}
+ if all("verdict" in v for v in record["variants"]) and (
+ record["kind"] == "run" or check.get("observable") == "frame_hashes"):
+ for variant in record["variants"]:
+ position.setdefault(variant["verdict"], len(position))
+ result[variant["label"]] = RESULT_WORDS[variant["verdict"]][0]
+ groups = {v["label"]: position[v["verdict"]] for v in record["variants"]}
else:
- lines += [f" **Result:** `{item['status']}`. {_text(item['statement'])}.", ""]
- if item.get("method"):
- lines += [f" **Method:** {_text(item['method'])}", ""]
- lines += [
- f" **Ran on:** {_text(item['host'])}, {item['date'][:10]}. Core build: {build}. "
- f"{item['variants']} variant{'s' if item['variants'] != 1 else ''}, "
- f"{item['repetitions']} repetition{'s' if item['repetitions'] != 1 else ''} each"
- + (f", {item['flags']} flag{'s' if item['flags'] != 1 else ''} raised" if item["flags"] else "")
- + ".",
- "",
- f" [Full record]({item['record_url']}), {runs}.",
- "",
+ groups = {}
+ for number, group in enumerate((check.get("result") or {}).get("groups") or ()):
+ for label in group["variants"]:
+ groups[label] = number
+ result[label] = f"result {chr(ord('A') + number % 26)}"
+ varied = check.get("variable")
+ return [{"variant": v["label"], "group": groups.get(v["label"], 0),
+ "result": result.get(v["label"], ""),
+ "absent": bool(varied) and varied in v["system_files"] and v["system_files"][varied] is None}
+ for v in record["variants"]]
+
+
+def proves(record_id: str, profile: dict) -> list[str]:
+ """The profile fields this record is evidence for, as `field of file`."""
+ out = []
+ for entry in profile.get("files") or ():
+ for name, ids in sorted((entry.get("evidence") or {}).items()):
+ if record_id in ids:
+ out.append(f"{name} of {entry.get('name')}")
+ return out
+
+
+# An MDX page reads a brace as the start of an expression, in text and in
+# an attribute alike.
+_BRACES = str.maketrans({"{": "{", "}": "}"})
+
+
+def _attr(value: object) -> str:
+ return html.escape(" ".join(str(value or "").split()), quote=True).translate(_BRACES)
+
+
+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."""
+ 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)
+
+
+def _dots(item: dict) -> str:
+ groups = item.get("groups") or []
+ names = "; ".join(f"{g['variant']}: {g['result']}" for g in groups)
+ dots = "".join(
+ f'' for g in groups)
+ return (f''
+ f"{dots}")
+
+
+def _chip(item: dict) -> str:
+ words, kind = RESULT_WORDS.get(item["status"], (item["status"], "muted"))
+ return f'{html.escape(words)}'
+
+
+def _sentence_html(label: str, value: object, markup_ready: bool = False) -> str:
+ """One line of the details, or nothing: an empty value is never printed."""
+ text = (str(value or "") if markup_ready else _cell(value)).strip().rstrip(".")
+ return f"
{label}: {text}.
" if text else ""
+
+
+def _details(item: dict) -> list[str]:
+ core = item["core"]
+ build = _cell(core["library"]) + (f" at {html.escape(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"
+ if item["flags"]:
+ reps += f", {item['flags']} flag{'s' if item['flags'] != 1 else ''} raised"
+ runs = "1 run" if item["runs"] == 1 else f"{item['runs']} runs, the latest shown"
+ lines = [
+ _sentence_html("Claim", item.get("claim")),
+ _sentence_html("Result", item.get("statement")),
+ _sentence_html("Method", item.get("method")),
+ _sentence_html("Proves", ", ".join(item.get("proves") or [])),
+ _sentence_html("Ran on", item["host"]),
+ _sentence_html("Core build", build and f"{build}. {reps}", markup_ready=True),
+ f'Full record, {runs}.
',
+ ]
+ return [line for line in lines if line]
+
+
+def tested_counts(items: list[dict]) -> dict:
+ tests = [i for i in items if i["kind"] != "bench"]
+ return {"tests": len(items), "confirmed": sum(1 for i in items if i.get("proves")),
+ "open": max((i.get("open_findings", 0) for i in items), default=0),
+ "benchmarks": len(items) - len(tests),
+ "last": max((i["date"][:10] for i in items), default="")}
+
+
+def core_label(item: dict) -> str:
+ return Path(str(item["core"].get("file") or "")).name.split("_libretro")[0]
+
+
+def _row(item: dict, page_host: str, main_core: str) -> list[str]:
+ elsewhere = (item.get("host_short") or item["host"]) != page_host
+ title = _cell(item.get("title") or short_title(item))
+ if core_label(item) != main_core:
+ title += f' {html.escape(core_label(item))}'
+ return [
+ '',
+ (""
+ f'{title}'
+ f'{_chip(item)}'
+ f'{_dots(item)}'
+ f'{item["date"][:10]}'
+ + (f' {_attr(item.get("host_short"))}' if elsewhere else "")
+ + "
"),
+ '',
+ *_details(item),
+ "
",
+ " ",
]
- return lines
def tested_section(name: str, items: list[dict]) -> list[str]:
- """The Tested block of an emulator page, empty when nothing was recorded."""
+ """The Tested block of an emulator page, empty when nothing was recorded.
+
+ The answer first: one summary line, then one row per test with its
+ result chip, a dot per variant and its date. What proves it (method,
+ claim, computer, core build, record) waits in each row's details. Tests
+ recorded before the boot oracle fold under one line, and benchmarks get
+ their own table at the end.
+ """
if not items:
return []
- lines = [
- "## Tested",
- "",
- f"This emulator was run in real conditions by the [core probe](../{METHOD_PAGE}). "
- f"Each test below is the latest of its kind. "
- f"[All measurements](../{EXPLORER_PAGE}), "
- f"[these records as SQLite](../downloads/measurements/{name}.sqlite).",
- "",
- ]
- for item in sorted(items, key=lambda i: (i["date"], i["subject"]), reverse=True):
- lines += tested_item(item)
- return lines
+ counts = tested_counts(items)
+ hosts = [i.get("host_short") or i["host"] for i in items]
+ page_host = max(sorted(set(hosts)), key=hosts.count)
+ cores = [core_label(i) for i in items]
+ main_core = max(sorted(set(cores)), key=cores.count)
+ tests = f"{counts['tests']} test{'s' if counts['tests'] != 1 else ''}"
+ summary = (f"{tests} · {counts['confirmed']} confirmed · {counts['open']} open · "
+ f"last run {counts['last']} on {page_host}.")
+ links = (f'How · '
+ f'All tests · '
+ 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"]
+ lines = ["## Tested", "", '',
+ f'
{html.escape(summary)} {links}
']
+ if current or earlier:
+ lines.append('
Test'
+ "ResultVariantsDate
")
+ for item in current:
+ lines += _row(item, page_host, main_core)
+ if earlier:
+ lines += ['
',
+ f"{len(earlier)} earlier test{'s' if len(earlier) != 1 else ''}, "
+ "before the boot oracle
"]
+ for item in earlier:
+ lines += _row(item, page_host, main_core)
+ lines.append(" ")
+ if benches:
+ # One line: MDX reads a table split over lines as paragraphs.
+ table = ['
| Benchmark | ms per frame | '
+ "fps | stdev | runs |
"]
+ for item in benches:
+ timing = item.get("timing") or {}
+ title = _cell(item.get("title") or short_title(item))
+ if core_label(item) != main_core:
+ title += f' {html.escape(core_label(item))}'
+ table.append(
+ f'| {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, "
"]))
+ return [*lines, "
", ""]
def hosts_table(index: dict) -> list[str]:
diff --git a/tests/test_measurements.py b/tests/test_measurements.py
index c7eb2536e..5bf12a97b 100644
--- a/tests/test_measurements.py
+++ b/tests/test_measurements.py
@@ -220,7 +220,8 @@ class ReadAndIndex(Workspace):
keys = {"emulator", "emulator_name", "profiled", "systems", "system_names", "subject", "kind",
"claim", "method", "observable", "variable", "status", "statement", "content",
"variants", "repetitions", "flags", "date", "host", "core", "frontend_commit", "id",
- "runs", "record_url", "sqlite", "verdicts"}
+ "runs", "record_url", "sqlite", "verdicts", "title", "host_short", "groups",
+ "proves", "timing"}
for item in index["items"]:
self.assertEqual(set(item), keys)
alpha = {i["subject"]: i for i in index["items"] if i["emulator"] == "alpha"}
@@ -247,11 +248,9 @@ class ReadAndIndex(Workspace):
self.assertNotIn("differ", statement)
# A check on another observable keeps its comparison.
self.assertEqual(sm.result_of(full_record("alpha", "c", "2026-10-06T10:00:00+00:00"))[0], "differs")
- item = {"claim": "c", "runs": 1, "core": {"library": "Fake 1", "commit": None}, "status": "mixed",
- "statement": "", "verdicts": sm.verdicts_of(record), "method": "m", "host": "h",
- "date": "2026-10-06", "variants": 2, "repetitions": 2, "flags": 0, "record_url": "u"}
- text = "\n".join(sm.tested_item(item))
- self.assertIn("v1: `hang`", text)
+ groups = sm.variant_groups(record)
+ self.assertEqual([(g["variant"], g["result"], g["group"]) for g in groups],
+ [("v0", "works", 0), ("v1", "hang", 1)])
def test_links_name_the_last_commit_that_touched_the_records(self) -> None:
first = self.repository()
@@ -431,12 +430,90 @@ class SitePages(unittest.TestCase):
self.assertEqual(sm.tested_section("beetle_saturn", []), [])
text = "\n".join(sm.tested_section("beetle_saturn", [self.ITEM]))
self.assertTrue(text.startswith("## Tested\n"))
- for expected in ("**Result:** `same`", "**Method:** Twice per card. Every input fixed.", "**Ran on:** Test CPU, 12 CPUs",
- self.ITEM["record_url"], "1 flag raised", "2 runs",
- "../downloads/measurements/beetle_saturn.sqlite", "../wiki/measurements.md",
- "<image>"):
+ for expected in ("no difference", "Method: Twice per card. Every input fixed.",
+ "Ran on: Test CPU, 12 CPUs", self.ITEM["record_url"],
+ "1 flag raised", "2 runs", "downloads/measurements/beetle_saturn.sqlite",
+ "wiki/measurements/", "<image>", "1 test · 0 confirmed · 0 open"):
self.assertIn(expected, text)
+ def item(self, **values: object) -> dict:
+ return {**self.ITEM, "host_short": "Test CPU, Linux", **values}
+
+ def test_summary_counts(self) -> None:
+ items = [self.item(proves=["hle_fallback of a.bin"], open_findings=2),
+ self.item(subject="b", date="2026-10-28T00:00:00+00:00", open_findings=2),
+ self.item(kind="bench", status="measured", subject="c",
+ timing={"ms_per_frame": 1.5, "fps": 660.0, "stdev": 0.01})]
+ self.assertEqual(sm.tested_counts(items), {"tests": 3, "confirmed": 1, "open": 2, "benchmarks": 1,
+ "last": "2026-10-28"})
+ text = "\n".join(sm.tested_section("beetle_saturn", items))
+ self.assertIn("3 tests · 1 confirmed · 2 open · last run 2026-10-28 on Test CPU, Linux.", text)
+ self.assertIn("1.500 | 660.0 | ", text)
+
+ def test_short_titles(self) -> None:
+ 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)"),
+ ({"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)"),
+ ({"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 ...)"),
+ ]
+ for item, title in cases:
+ self.assertEqual(sm.short_title(item), title)
+ self.assertEqual(sm.short_host({"cpu_model": "13th Gen Intel(R) Core(TM) i9-13900K",
+ "os": "Linux-6.18.54.1-microsoft-standard-WSL2-x86_64"}),
+ "Intel Core i9-13900K, Linux (WSL2)")
+
+ 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")]))
+ self.assertNotRegex(text, r"\s*\.|:\s*\.\s|`ran`\. \.")
+ self.assertNotIn("Claim:", text)
+ self.assertNotIn("Method:", text)
+
+ def test_dots_follow_the_result_groups(self) -> None:
+ record = full_record("alpha", "boot", "2026-10-06T10:00:00+00:00", values=("1" * 40, "2" * 40, "1" * 40))
+ record["check"]["variable"] = "card.bin"
+ record["variants"][2]["system_files"] = {"card.bin": None}
+ groups = sm.variant_groups(record)
+ self.assertEqual([g["group"] for g in groups],
+ [i for i, group in enumerate(record["check"]["result"]["groups"])
+ for _ in group["variants"]][:1] + [1, 0])
+ self.assertEqual([g["absent"] for g in groups], [False, False, True])
+ text = "\n".join(sm.tested_section("alpha", [self.item(groups=groups)]))
+ self.assertEqual(re.findall(r'rb-dot rb-dot-(\d)( rb-dot-empty)?"', text),
+ [("0", ""), ("1", ""), ("0", " rb-dot-empty")])
+ self.assertIn('aria-label="3 variants. v0: result A; v1: result B; v2: result A"', text)
+
+ def test_the_details_hold_the_method_and_the_host(self) -> None:
+ text = "\n".join(sm.tested_section("beetle_saturn", [self.item()]))
+ details = text.split('', 1)[1].split("
", 1)[0]
+ self.assertIn("Twice per card", details)
+ self.assertIn("Test CPU, 12 CPUs", details)
+ self.assertIn(self.ITEM["record_url"], details)
+
+ def test_the_beetle_saturn_block_stays_short(self) -> None:
+ stored = mr.read_records(REPO_ROOT)
+ profiles = sm.load_profiles(REPO_ROOT / "emulators")
+ index = sm.build_index(stored, {"repo": REPO_URL, "commit": COMMIT}, profiles)
+ items = sm.by_emulator(index).get("beetle_saturn")
+ if not items:
+ self.skipTest("no beetle_saturn record")
+ block = "\n".join(sm.tested_section("beetle_saturn", items))
+ # What a reader sees before opening anything: the closed details hide their body.
+ shown = re.sub(r'.*?
', " ", block, flags=re.S)
+ shown = re.sub(r'.*? \s*', " ", shown,
+ flags=re.S)
+ shown = re.sub(r"<[^>]+>", " ", shown)
+ self.assertLess(len(re.findall(r"[A-Za-z0-9][\w.'-]*", shown)), 120, shown)
+
def test_hosts_table_on_the_method_page(self) -> None:
page = sm.with_hosts(f"x\n\n{sm.HOSTS_MARKER}\n",
{"items": [self.ITEM, {**self.ITEM, "host": "Test CPU, 12 CPUs, load 1 before, 2 after"}]})
diff --git a/website/src/styles/retrobios.css b/website/src/styles/retrobios.css
index 77831fc28..865b72e04 100644
--- a/website/src/styles/retrobios.css
+++ b/website/src/styles/retrobios.css
@@ -877,3 +877,78 @@ starlight-tabs [role='tab'] { white-space: nowrap; }
@media (min-width: 50rem) {
header .site-title img { height: calc(4rem - 2 * var(--sl-nav-pad-y)); width: auto; }
}
+
+/* The Tested block of an emulator page (scripts/sitemeasurements.py): a
+ summary line, then one row per test, each a whose summary is
+ the row and whose body is the proof. Rows stack into two lines on a
+ phone. A dot per variant, its group in colour and its name in a title
+ and the strip's label; a variant that staged no file is a ring. */
+.sl-markdown-content .rb-tested-summary { color: var(--rb-text-secondary); font-size: 0.9rem; }
+.sl-markdown-content :is(.rb-tested-head, .rb-tested-row > summary) {
+ display: grid;
+ grid-template-columns: 1.25rem minmax(0, 1fr) 8.5rem 9rem 6rem;
+ gap: 0.5rem;
+ align-items: center;
+}
+.sl-markdown-content .rb-tested-head {
+ margin-top: 0.75rem;
+ padding: 0 0 0.25rem;
+ border-bottom: 1px solid var(--rb-border-strong);
+ color: var(--rb-text-secondary);
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+.sl-markdown-content .rb-tested-head::before { content: ""; }
+/* Starlight draws a side rule on every details: a row has a rule below only. */
+.sl-markdown-content details:is(.rb-tested-row, .rb-tested-earlier) { border-inline-start: 0; padding-inline-start: 0; }
+.sl-markdown-content details.rb-tested-row { margin: 0; border-bottom: 1px solid var(--rb-border); }
+.sl-markdown-content details.rb-tested-row:has(> summary:hover) { border-color: var(--rb-border); }
+.sl-markdown-content .rb-tested-row > summary {
+ min-height: 2.25rem;
+ padding: 0.3rem 0;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+.sl-markdown-content .rb-tested-row > summary::before { background-color: var(--rb-text-secondary); margin: 0; }
+.sl-markdown-content .rb-tested-row > summary:hover { background: var(--rb-surface); }
+.sl-markdown-content .rb-tested-test { font-weight: 600; overflow-wrap: anywhere; }
+.sl-markdown-content :is(.rb-tested-test, .rb-tested-date) small { color: var(--rb-text-secondary); font-weight: 400; }
+.sl-markdown-content .rb-tested-date { color: var(--rb-text-secondary); font-variant-numeric: tabular-nums; }
+.sl-markdown-content .rb-tested-more {
+ margin: 0 0 0.75rem 1.75rem;
+ padding: 0.5rem 0.75rem;
+ border-left: 2px solid var(--rb-border-strong);
+ font-size: 0.875rem;
+}
+.sl-markdown-content .rb-tested-more p { margin: 0.35rem 0 0; }
+.sl-markdown-content .rb-tested-more p:first-child { margin-top: 0; }
+.sl-markdown-content details.rb-tested-earlier { margin-top: 0.75rem; }
+.sl-markdown-content details.rb-tested-earlier > summary { font-size: 0.9rem; color: var(--rb-text-secondary); cursor: pointer; }
+.sl-markdown-content .rb-dots { display: inline-flex; flex-wrap: wrap; gap: 3px; }
+.sl-markdown-content .rb-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ border: 2px solid var(--rb-dot);
+ background: var(--rb-dot);
+}
+.sl-markdown-content .rb-dot-empty { background: transparent; }
+.sl-markdown-content .rb-dot-0 { --rb-dot: var(--rb-ok-fg); }
+.sl-markdown-content .rb-dot-1 { --rb-dot: var(--rb-info-fg); }
+.sl-markdown-content .rb-dot-2 { --rb-dot: var(--rb-warn-fg); }
+.sl-markdown-content .rb-dot-3 { --rb-dot: var(--rb-err-fg); }
+.sl-markdown-content .rb-dot-4 { --rb-dot: var(--rb-muted-fg); }
+.sl-markdown-content table.rb-tested-bench { margin-top: 1rem; font-size: 0.875rem; }
+.sl-markdown-content table.rb-tested-bench td:not(:first-child) { font-variant-numeric: tabular-nums; text-align: right; }
+@media (max-width: 50rem) {
+ .sl-markdown-content div.rb-tested-head { display: none; }
+ .sl-markdown-content details.rb-tested-row > summary {
+ grid-template-columns: 1.25rem auto auto minmax(0, 1fr);
+ row-gap: 0.2rem;
+ }
+ .sl-markdown-content .rb-tested-row > summary::before { grid-row: 1; }
+ .sl-markdown-content .rb-tested-test { grid-column: 2 / -1; }
+ .sl-markdown-content .rb-tested-result { grid-column: 2; }
+ .sl-markdown-content .rb-tested-date { text-align: right; }
+ .sl-markdown-content .rb-tested-more { margin-left: 0.5rem; }
+}
diff --git a/wiki/measurements.md b/wiki/measurements.md
index b251058d0..a39682963 100644
--- a/wiki/measurements.md
+++ b/wiki/measurements.md
@@ -14,34 +14,35 @@ the emulator's page shows a **Tested** block. Every result is on the
## What a test does
- A script runs the emulator with no window and no sound.
-- It starts a game and runs it for a set time.
-- Each run starts fresh and runs at least twice.
-- Each run gets one result, by comparing its screens with ones we checked by eye.
+- It starts a game and runs it for a set time, at least twice.
+- It compares the screens with ones we checked by eye.
## What a result says
-- **boots**: the game reached a screen it shows when it works.
-- **bios_error**: the emulator said a BIOS is missing or wrong.
-- **crash**: the emulator stopped.
-- **hang**: the screen stayed blank, or the run never ended.
-- **unknown**: none of these could be shown.
-- **measured**: a speed test, given as time per frame.
-- **no verdict**: an older test that only compared screens.
+| Result | Meaning |
+|--------|---------|
+| works | the game reached a screen it shows when it works |
+| BIOS error | the emulator said a BIOS is missing or wrong |
+| crash | the emulator stopped |
+| hang | the screen stayed blank, or the run never ended |
+| unknown | none of these could be shown |
+| no verdict | an older test that only compared screens |
+| ms per frame | a speed test |
## How a file is proven
-The game runs with no BIOS, with the BIOS alone, and with a wrong file under
-its name: zeros, half the file, or another region's BIOS.
+The game runs with no BIOS and with the BIOS alone. Then it runs with a wrong
+file under the BIOS name: zeros, half the file, or another region's BIOS.
-- **Required**: no BIOS fails, the BIOS alone boots.
-- **Built-in stand-in**: no BIOS still boots.
+- **Required**: no BIOS fails, the BIOS alone works.
+- **Built-in stand-in**: no BIOS still works.
- **Checked**: a wrong file is refused.
## When tests run
-A maintainer starts a test run. A clear result updates the emulator's page and
-names the test as proof. Anything else waits on the
-[Measurements](../measurements.md) page for someone to read the code.
+A maintainer starts a test run. A clear result updates the emulator's page.
+Anything else waits on the [Measurements](../measurements.md) page for someone
+to read the code.
## What a result keeps