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('") + 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 = ['' + ""] + 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'' + f'' + f'') + lines.append("".join([*table, "
Benchmarkms per framefpsstdevruns
{title}{timing.get("ms_per_frame", 0):.3f}{timing.get("fps", 0):.1f}{timing.get("stdev", 0):.3f}{item["repetitions"]}
"])) + 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.500660.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