feat: round 4 entry pages

This commit is contained in:
Abdessamad Derraz committed 2026-10-23 19:23:29 +02:00
commit 03d12ffe98
7 files changed
+361 -26

No files matched your search

+28 -3
View File
@@ -74,6 +74,11 @@ device_groups:
- name: Linux handhelds
text: ArkOS, muOS, Onion, spruceOS
link: platforms/linux-handhelds.md
# No terminal runs there: the files are copied in, each app its own way
# (which-pack.md, section iPhone and iPad, each step cited).
- name: iPhone and iPad
text: RetroArch, Delta, Provenance
link: which-pack.md#iphone-and-ipad
fpga:
label: MiSTer FPGA
description: >-
@@ -130,7 +135,8 @@ terminals:
# A folder is the one seen on the card from a computer, and a device list the
# one the system's own site, wiki or README gives. Each is written only with
# the first-party page that states it (`*_source`), and left out when no such
# page states it: the row then says the command finds the folder.
# page states it: the row then says where RetroArch shows the folder.
# `linux_only` marks a folder on a part of the card only Linux reads.
handheld_guide:
page: linux-handhelds
title: Linux handhelds
@@ -162,12 +168,15 @@ handheld_guide:
TrimUI Smart Pro, TrimUI Brick, Anbernic RG35XX Plus, RG40XX H, Powkiddy RGB30]
devices_source: https://github.com/spruceUI/spruceOS/wiki/Supported-Devices
- name: Allium
folder: '`BIOS` at the root of the card'
folder_source: https://github.com/goweiwen/Allium/tree/ee0dda979bc02649fdda48b277eaa60938273465/static/BIOS
devices: [Miyoo Mini, Miyoo Mini Plus, Miyoo Mini Flip]
devices_source: https://github.com/goweiwen/Allium
- name: Anbernic stock firmware
- name: ArchR
folder: '`/storage/roms/bios`, on a part Windows and macOS cannot open'
folder: '`/storage/roms/bios`, on a part only Linux can open'
folder_source: https://arch-r.io/docs/roms/
linux_only: true
devices: [R36S, R33S, R36S Soysauce, Odroid Go Advance, Odroid Go Super, RG351V,
RG351M, GameForce Chi, MagicX XU10, Powkiddy RGB10, Powkiddy RGB10X, K36]
devices_source: https://github.com/archr-linux/Arch-R
@@ -182,7 +191,11 @@ handheld_guide:
devices: [Odroid Go Advance, RGB10, RG351MP, G350]
devices_source: https://github.com/christianhaitian/dArkOS/wiki
- name: JELOS
folder: '`roms/bios` on the games card'
folder_source: https://github.com/JustEnoughLinuxOS/distribution/blob/7b2e116ce6996d56e642a0e2a07288c926db926b/packages/emulators/standalone/retroarch/sources/RK3566/retroarch.cfg#L716
- name: KOLOS
folder: '`roms/bios` on the games card'
folder_source: https://github.com/AnonLinuxOS/KOLOS/blob/7daf5950ae79bb8775f129468164f730dbc32298/packages/emulators/standalone/retroarch/sources/RK3566/retroarch.cfg#L716
devices: [RG353P, RG353M, RG353V, RG353VS, RG503, Powkiddy RK2023, RGB30, RGB10 Max 3]
devices_source: https://github.com/AnonLinuxOS/KOLOS
- name: Koriki
@@ -191,6 +204,9 @@ handheld_guide:
devices: [Miyoo Mini]
devices_source: https://github.com/Rparadise-Team/Koriki
- name: MiyooCFW
folder: '`.retroarch/system`, hidden, on the **MAIN** part, which only Linux can open'
folder_source: https://github.com/MiyooCFW/buildroot/blob/d5912eeb7b636810bafaa34eb0517fd599a0fc71/board/miyoo/rootfs/etc/main#L164-L170
linux_only: true
devices: [BittBoy, Pocket Go, PowKiddy Q90, PowKiddy V90, PowKiddy Q20 Mini, Sup M3, XYC Q8]
devices_source: https://github.com/MiyooCFW/docs/blob/main/docs/index.md
- name: plumOS
@@ -521,6 +537,11 @@ guides:
runs_on: Anbernic RG351 and RG552
folders:
- where: On AmberELEC
# AmberELEC 5a3882cb packages/network/samba/config/smb.conf:59-60 shares
# /storage/roms as "roms". smbd runs unless samba.disabled exists
# (system.d.opt/smbd.service:5).
- where: From Windows, over your home network
path: '`\\AmberELEC\roms\`'
run: [device]
# amberelec.org/guides/getting-to-know-amberelec.html: host AmberELEC, root, amberelec.
terminal: >-
@@ -569,7 +590,8 @@ guides:
run: [device]
notes:
- >-
For the network folder, turn on **SAMBA** in **System Settings > Services**.
For the network folder, turn on **SAMBA** in **System Settings > Services**
([KNULLI docs](https://github.com/knulli-cfw/knulli.org/blob/314b26f1822c8531bed8a9cea72028b229b4436e/docs/play/add-games/network-transfer.md?plain=1#L20-L24)).
# knulli.org/configure/ssh: SSH in System Settings > Services, root, password linux.
terminal: >-
Turn on **SSH**, a terminal over the network, in **System Settings > Services**. From
@@ -687,6 +709,7 @@ guides:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
- The command makes each console's folder for you.
on_card: true
reference_notes:
- >-
@@ -711,6 +734,7 @@ guides:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
- The command makes each console's folder for you.
on_card: true
reference_notes:
- >-
@@ -730,6 +754,7 @@ guides:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
- The command makes each console's folder for you.
on_card: true
reference_notes:
- >-
+107 -15
View File
@@ -531,7 +531,7 @@ def generate_home(
"",
"- BIOS missing, even with the file there? "
"[When a game does not start](wiki/troubleshooting.md)",
"- Reference: [Check your file](files/index.md)",
"- Have a file? [Check it](files/index.md)",
"",
"## Do I need it?",
"",
@@ -585,9 +585,9 @@ def generate_explore_index(db: dict, coverages: dict, profiles: dict) -> str:
"[All installer options](wiki/installer.md)"]),
("How files are checked", [
"Each setup checks its files its own way: by name, by MD5 or by SHA1.", "",
"Reference: [How each platform checks files](wiki/verification-modes.md)"]),
"[How each platform checks files](wiki/verification-modes.md)"]),
("Data and API", ["Every file and hash as JSON, CSV and SQLite.", "",
"Reference: [Data and API](data.md)"]),
"[Data and API](data.md)"]),
]),
"## The collection",
"",
@@ -2805,21 +2805,25 @@ def generate_platform_index(coverages: dict, registry: dict | None = None) -> st
lines = [
"# Your setup",
"",
"Each setup's page gives the folder, the command and the ZIP.",
"Each setup's page gives the folder and the command.",
"",
]
for group, (label, description) in DEVICE_GROUPS.items():
for group, (label, _description) in DEVICE_GROUPS.items():
members = _group_members(coverages, registry, group)
if not members:
continue
# One setup: its card says what the group description would.
lines += [f"## {label}", "", *([description, ""] if len(members) > 1 else [])]
# The group label names the devices, and each card carries its
# setup's own sentence. The kinds of device that run a setup
# follow, as on the home tiles.
lines += [f"## {label}", ""]
lines += markup.card_grid([
(f"[{coverages[name]['platform']}]({name}.md)", [GUIDES[name].summary])
for name in members
*((f"[{coverages[name]['platform']}]({name}.md)", [GUIDES[name].summary])
for name in members),
*((f"[{tile.name}](../{tile.link})", [tile.text])
for tile in GROUP_TILES.get(group, ())),
])
lines += [
f"Reference: [Every setup side by side](../{SETUP_TABLE_LINK})",
f"Comparing setups? [Every setup side by side](../{SETUP_TABLE_LINK})",
"",
]
return "\n".join(lines) + "\n"
@@ -3418,6 +3422,26 @@ SAFE_LINK = "[Is it safe?](../index.md#is-it-safe)"
OFFLINE_LINE = ("No internet on the device? "
"[Install from the ZIP](../wiki/installer.md#without-a-network-offline-install).")
def _network_share(name: str, guide: Guide) -> str:
"""The network folder a computer fills, for a setup that runs on its own
device and has no ZIP: the command runs on the computer and writes
there. Empty when the page cites no such folder."""
if guide.run != ("device",) or _computer_first(name, guide):
return ""
for label, path in guide.folders:
if label.startswith("From Windows") and path.startswith("`\\\\"):
return path.strip("`").rstrip("\\")
return ""
def _offline_line(name: str) -> list[str]:
"""The offline line, on a setup whose ZIP a release publishes: the
installer reads that ZIP with no network (`--from-pack`)."""
pack = release_record.pack_for(name, release_record.load_advertised())
return [OFFLINE_LINE, ""] if pack and pack.get("assets") else []
# Shown in the "On the device" tab of a guide that has no `terminal:` of its own.
DEVICE_TERMINAL_FALLBACK = "Or fill the folder from your computer, with the ZIP."
@@ -3528,13 +3552,26 @@ def devices_meta(devices: tuple[str, ...]) -> str:
HANDHELD_ROW_DEVICES = 6
# Where a handheld row with no cited folder sends the reader: the setting
# that shows RetroArch's BIOS folder, the words of the RetroArch page.
RETROARCH_FOLDER_SETTING = ("In RetroArch, open **Settings > Directory > System/BIOS**. "
"It shows the folder.")
# After a folder on a part of the card only Linux reads.
LINUX_ONLY_HINT = ("Copy from a Linux computer, or check the folder in RetroArch's "
"**Settings > Directory > System/BIOS**.")
def _handheld_row(system: HandheldSystem) -> str:
shown = ", ".join(system.devices[:HANDHELD_ROW_DEVICES])
more = len(system.devices) - HANDHELD_ROW_DEVICES
devices = (f"{shown}{f' and {more} more' if more > 0 else ''} "
f"([list]({system.devices_source}))" if system.devices else "")
folder = (f"{system.folder} ([source]({system.folder_source}))" if system.folder
else "Not written by the system. The command finds it on the device.")
if not system.folder:
folder = RETROARCH_FOLDER_SETTING
else:
folder = f"{system.folder} ([source]({system.folder_source}))"
if system.linux_only:
folder += f". {LINUX_ONLY_HINT}"
return f"| {system.name} | {devices} | {folder} |"
@@ -3558,6 +3595,8 @@ def generate_handheld_guide(guide: HandheldGuide) -> str:
"",
devices_meta(guide.devices),
"",
"[Download](#download)",
"",
"## Where the files go { #where-the-files-go }",
"",
"Put the SD card in a computer. Find your system below.",
@@ -3567,6 +3606,7 @@ def generate_handheld_guide(guide: HandheldGuide) -> str:
*(_handheld_row(system) for system in guide.systems),
"",
*_guide_download(guide.uses, display, uses, None, "## Download { #download }"),
*_offline_line(guide.uses),
"## Fill the card from a computer { #install }",
"",
"On Windows, note the card's letter, like **E:**. Put its BIOS folder after "
@@ -3612,18 +3652,31 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
])
if guide.on_card:
lines += ["<!-- image: minui-card -->", ""]
share = _network_share(name, guide)
if _computer_first(name, guide):
lines += [
*_guide_download(name, display, guide, extract_to,
"## From your computer { #download }"),
OFFLINE_LINE,
*_offline_line(name),
*_guide_install(name, guide, "## On the device { #install }"),
]
elif share:
lines += [
"## From your computer { #from-your-computer }",
"",
"Needs the device on your network. Run the command on the computer.",
"",
*terminal_tabs(("windows",), {"windows": f"--platform {name} --dest {share}"}),
SAFE_LINK,
"",
*_guide_install(name, guide, "## On the device { #install }"),
*_guide_download(name, display, guide, extract_to, "## Download"),
]
else:
lines += [
*_guide_install(name, guide, "## Install"),
*_guide_download(name, display, guide, extract_to, "## Download"),
*_offline_line(name),
]
check_args = {key: f"{args} --check" for key, args in platform_args(name, guide).items()}
lines += [
@@ -8665,6 +8718,44 @@ def _folder_tables(registry: dict, record: dict, display: dict[str, str]) -> lis
return lines
# iPhone and iPad: no terminal, so each app is filled its own way. Every
# step is the app's own: RetroArch's iOS guide (Files app, On My iPhone >
# RetroArch) and platform_darwin.m (system under Documents/RetroArch), the
# DS BIOS rows of Delta's Settings.storyboard, Provenance's import guide.
IOS_SOURCES = {
"retroarch_docs": "https://github.com/libretro/docs/blob/"
"cfa4c1a17e586c01d3a48e9eda12b6c989f8954f/docs/guides/install-ios.md?plain=1#L94",
"retroarch_code": "https://github.com/libretro/RetroArch/blob/"
"5333a7c5a59e268470834a96f86047d3cb6139a9/frontend/drivers/platform_darwin.m#L438-L459",
"delta": "https://github.com/rileytestut/Delta/blob/"
"c1d3d068e019e6493eed45654569db3cc5beb86a/Delta/Base.lproj/Settings.storyboard#L2448-L2500",
"provenance": "https://wiki.provenance-emu.com/using-provenance/importing-roms",
}
IOS_DEVICES = ("iPhone", "iPad", "iOS", "Delta", "Provenance")
def ios_section() -> list[str]:
"""The section the iPhone and iPad tile opens: the consoles picked on a
computer, then each app's own way in."""
return [
"## iPhone and iPad { #iphone-and-ipad }",
"",
devices_meta(IOS_DEVICES),
"",
"[Pick the consoles](which-pack.md?platform=retroarch#only-some-consoles) "
"on a computer. Copy them to the app.",
"",
"- **RetroArch**: Files app, **On My iPhone > RetroArch**, then "
f"`RetroArch/system/` ([docs]({IOS_SOURCES['retroarch_docs']}), "
f"[code]({IOS_SOURCES['retroarch_code']})).",
"- **Delta**, for DS games: import `bios7.bin`, `bios9.bin` and `firmware.bin` "
f"in **Settings**, under **Nintendo DS** ([code]({IOS_SOURCES['delta']})).",
"- **Provenance**: share the file, then **Copy to Provenance** "
f"([docs]({IOS_SOURCES['provenance']})).",
"",
]
def generate_which_pack() -> str:
"""The device picker: every registered platform as a tile under the
device it runs on, the ZIP advice with every folder and ZIP in one closed
@@ -8679,7 +8770,7 @@ def generate_which_pack() -> str:
lines = [
"# Get your files",
"",
"Pick your setup. Its page gives the folder, the command and the ZIP.",
"Pick your setup. Its page gives the folder and the command.",
"",
"## Pick your setup",
"",
@@ -8713,6 +8804,7 @@ def generate_which_pack() -> str:
"",
"<FilePicker />",
"",
*ios_section(),
"## Not in the list? One command finds it { #quick-install }",
"",
"A **terminal** is a window where you type commands.",
@@ -8723,7 +8815,7 @@ def generate_which_pack() -> str:
"",
"The command asks what to install. Press **Enter** for everything.",
"",
"```text title=\"What it asks\"",
"```text title=\"What it asks\" frame=\"terminal\"",
" Enter) install everything",
" i) only what the installed cores read",
" c) choose systems, cores and regions",
+6 -1
View File
@@ -91,6 +91,7 @@ class HandheldSystem:
folder_source: str = ""
devices: tuple[str, ...] = ()
devices_source: str = ""
linux_only: bool = False
@dataclass(frozen=True)
@@ -125,7 +126,7 @@ _GROUP_KEYS = {"label", "description", "anchor", "also_runs", "tiles"}
_ALSO_KEYS = ("setup", "summary", "uses", "runs_on", "folder")
_TILE_KEYS = ("name", "text", "link")
_HANDHELD_KEYS = {"page", "title", "summary", "uses", "systems"}
_SYSTEM_KEYS = {"name", "folder", "folder_source", "devices", "devices_source"}
_SYSTEM_KEYS = {"name", "folder", "folder_source", "devices", "devices_source", "linux_only"}
_SOURCE = re.compile(r"https://\S+")
_TERMINAL_KEYS = ("label", "how", "shell")
@@ -239,6 +240,10 @@ def _handheld_system(data: object, where: str) -> HandheldSystem:
values[value] = (_texts(data[value], f"{where}.{value}") if value == "devices"
else _text(data[value], f"{where}.{value}"))
values[source] = _source(data[source], f"{where}.{source}")
if "linux_only" in data:
if not isinstance(data["linux_only"], bool) or "folder" not in data:
raise GuideError(f"{where}.linux_only: expected true or false, with a folder")
values["linux_only"] = data["linux_only"]
return HandheldSystem(**values)
+4 -1
View File
@@ -185,7 +185,10 @@ def card_grid(cards: list[tuple[str, list[str]]]) -> list[str]:
def initials(name: str) -> str:
"""Up to two capitals of a name, for a setup that has no logo: MyMinUI is MM."""
"""Up to two capitals of a name, for a setup that has no logo: MyMinUI is MM.
A name written from a lowercase prefix keeps it: iPhone is iP, not PP."""
if len(name) > 1 and name[0].islower() and name[1].isupper():
return name[:2]
capitals = [c for c in name if c.isupper()]
return "".join(capitals[:2]) or name[:2].upper()
+7 -1
View File
@@ -199,7 +199,11 @@ class DeepLinksCarryALabel(unittest.TestCase):
"""A beginner page sent its reader to Tools and Architecture as the next
step. A link from a start or setup page to a page two levels deeper or
more says what it leads to, with that page's level label before it on
its line, or in the heading of its section ("## Reference: ...")."""
its line, or in the heading of its section ("## Reference: ...").
A link right after the question it answers ("Have a file? [Check it]")
says it too: there a label read as a broken crumb beside the Reference
tab (round 4)."""
def test_start_and_setup_pages(self):
import sitestarlight # noqa: PLC0415
@@ -229,6 +233,8 @@ class DeepLinksCarryALabel(unittest.TestCase):
if ORDER.index(deeper) - ORDER.index(level) < 2:
continue
label = f"{generate_site.LEVELS[deeper]}:"
if line[: match.start()].rstrip().endswith("?"):
continue
if label not in line[: match.start()] and not any(
heading.startswith(label) for heading in headings.values()):
offenders.append(f"{page.relative_to(DOCS)}: {match.group(0)}")
+8 -5
View File
@@ -49,7 +49,8 @@ def _display(key: str) -> str:
class HandheldWayIn(unittest.TestCase):
def test_android_and_linux_tiles_on_home_and_get_your_files(self):
tiles = siteguides.GROUP_TILES.get("handheld", ())
self.assertEqual([tile.name for tile in tiles], ["Android", "Linux handhelds"])
self.assertEqual([tile.name for tile in tiles],
["Android", "Linux handhelds", "iPhone and iPad"])
for name in ("index.md", "which-pack.md"):
text = _page(name)
with self.subTest(page=name):
@@ -228,10 +229,11 @@ class GetYourFilesListsEachSetupOnce(unittest.TestCase):
self.assertRegex(row, r"</a> \(\d[\d.]* [KMG]B\) \|$")
def test_the_zip_note(self):
text = re.sub(r"</?abbr[^>]*>", "", self.text)
for line in ("A big ZIP comes in parts, like `Pack.part1of2.zip`. Download every part.",
"Leave the ZIPs inside, like `neogeo.zip`, as they are.",
"Their setup's page says where to unzip them."):
self.assertIn(line, self.text)
self.assertIn(line, text)
self.assertNotIn("**Unzip into** means", self.text)
@@ -264,9 +266,10 @@ class HomeCopy(unittest.TestCase):
home = re.sub(r"</?abbr[^>]*>", "", _page("index.md"))
self.assertIn("- **Yes** for PlayStation 1 and 2, Saturn and Neo Geo.", home)
self.assertIn("- **Better with it** for Dreamcast: some games need it.", home)
# The level label opens its line, never the middle of a sentence.
self.assertNotIn("? Reference:", home)
self.assertIn("- Reference: [Check your file]", home)
# A question and the link that answers it: no level label, which
# read as a broken crumb beside the Reference tab (round 4).
self.assertNotIn("Reference:", home.split("\n---\n", 1)[-1])
self.assertIn("- Have a file? [Check it]", home)
self.assertIn("BIOS missing, even with the file there?", home)
def test_flycast_boots_without_the_dreamcast_bios(self):
+201
View File
@@ -0,0 +1,201 @@
"""Entry pages and guides (round 4, package 4).
Six handheld setups have no ZIP, and three of them offered SSH as root as
the only route while their page already named a network folder. Six rows
of the Linux handhelds table said "The command finds it on the device" for
devices with no terminal, while the installer held each folder with its
source. The offline line sat on six setup pages, the home page carried a
stray "Reference:" label, the Your setup index left out the handheld tiles,
and an iPhone owner found one FAQ line.
"""
from __future__ import annotations
import re
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
sys.path.insert(0, str(REPO_ROOT))
import generate_site # noqa: E402
import release_record # noqa: E402
import siteguides # noqa: E402
import sitemarkup # noqa: E402
from common import load_platform_registry # noqa: E402
from tests import sitepages # noqa: E402
REGISTRY = load_platform_registry(str(REPO_ROOT / "platforms"))
HANDHELDS = siteguides.HANDHELD_GUIDE
OFFLINE_ANCHOR = "#without-a-network-offline-install"
def _page(name: str) -> str:
sitepages.require()
return re.sub(r"</?abbr[^>]*>", "", sitepages.page(name).read_text(encoding="utf-8"))
def _guide_part(text: str) -> str:
match = re.search(r"^## Reference\b", text, re.M)
return text[: match.start()] if match else text
def _with_zip(name: str) -> bool:
pack = release_record.pack_for(name, release_record.load_advertised())
return bool(pack and pack.get("assets"))
SETUPS = [name for name in siteguides.GUIDES if name in REGISTRY]
class NoZipNoUnzipText(unittest.TestCase):
def test_a_setup_without_a_zip_never_says_unzip_or_download_pack(self):
for name in SETUPS:
if _with_zip(name):
continue
text = _guide_part(_page(f"platforms/{name}.md"))
with self.subTest(setup=name):
self.assertNotIn("Unzip the pack into", text)
self.assertNotIn("pack holds its own", text)
self.assertNotRegex(text, r"Download \S+ Pack")
self.assertNotIn("/releases/download/", text)
def test_the_minui_family_makes_each_folder(self):
for name in ("minui", "myminui", "nextui"):
with self.subTest(setup=name):
self.assertIn("The command makes each console's folder for you.",
_page(f"platforms/{name}.md"))
def test_knulli_is_filled_over_the_network_with_the_samba_switch_cited(self):
text = _guide_part(_page("platforms/knulli.md"))
self.assertIn("## From your computer { #from-your-computer }", text)
self.assertIn("--platform knulli --dest \\\\KNULLI\\share\\bios\n", text)
self.assertRegex(text, r"turn on \*\*SAMBA\*\* in \*\*System Settings > Services\*\*\s*"
r"\(\[KNULLI docs\]\(https://github\.com/knulli-cfw/knulli\.org/blob/"
r"[0-9a-f]{40}/docs/play/add-games/network-transfer\.md")
# The computer route comes before the SSH one.
self.assertLess(text.index("## From your computer"), text.index("## On the device"))
def test_the_elec_systems_say_they_need_the_network(self):
for name, share in (("amberelec", "\\\\AmberELEC\\roms"), ("emuelec", "\\\\EMUELEC\\roms")):
text = _guide_part(_page(f"platforms/{name}.md"))
with self.subTest(setup=name):
self.assertIn("Needs the device on your network.", text)
self.assertIn(f"--platform {name} --dest {share}\n", text)
def test_the_network_route_needs_a_share_and_no_zip(self):
for name in SETUPS:
share = generate_site._network_share(name, siteguides.GUIDES[name])
with self.subTest(setup=name):
if _with_zip(name) or siteguides.GUIDES[name].run != ("device",):
self.assertEqual(share, "")
if share:
self.assertTrue(share.startswith("\\\\"))
self.assertFalse(share.endswith("\\"))
class EveryHandheldRowSaysWhere(unittest.TestCase):
def test_no_row_sends_the_reader_to_a_command(self):
text = _page(HANDHELDS.path)
self.assertNotIn("The command finds it on the device", text)
rows = [line for line in text.splitlines() if line.startswith("| ")][1:]
self.assertEqual(len(rows), len(HANDHELDS.systems))
for row in rows:
folder = row.rstrip(" |").rsplit(" | ", 1)[-1]
with self.subTest(row=row[:40]):
self.assertTrue("([source](https://" in folder
or folder == generate_site.RETROARCH_FOLDER_SETTING, folder)
def test_the_installer_folders_are_written_with_their_source(self):
systems = {s.name: s for s in HANDHELDS.systems}
for name in ("Allium", "JELOS", "KOLOS", "MiyooCFW"):
with self.subTest(system=name):
self.assertTrue(systems[name].folder)
self.assertRegex(systems[name].folder_source, r"/[0-9a-f]{40}/")
def test_a_folder_only_linux_reads_says_what_to_do(self):
text = _page(HANDHELDS.path)
for system in HANDHELDS.systems:
row = next(line for line in text.splitlines() if line.startswith(f"| {system.name} |"))
with self.subTest(system=system.name):
self.assertEqual(generate_site.LINUX_ONLY_HINT in row, system.linux_only)
self.assertTrue(any(s.linux_only for s in HANDHELDS.systems if s.name == "ArchR"))
self.assertNotIn("Windows and macOS cannot open", text)
def test_linux_only_needs_a_folder(self):
with self.assertRaises(siteguides.GuideError):
siteguides._handheld_system({"name": "X", "linux_only": True}, "test")
def test_download_is_one_jump_under_the_lead(self):
text = _page(HANDHELDS.path)
self.assertIn("[Download](#download)", text)
self.assertLess(text.index("[Download](#download)"), text.index("## Where the files go"))
class OfflineLineWithEveryZip(unittest.TestCase):
def test_every_setup_with_a_zip_links_the_offline_install(self):
for name in SETUPS:
text = _guide_part(_page(f"platforms/{name}.md"))
with self.subTest(setup=name):
self.assertEqual(OFFLINE_ANCHOR in text, _with_zip(name))
def test_the_handheld_guide_links_it(self):
self.assertEqual(OFFLINE_ANCHOR in _page(HANDHELDS.path), _with_zip(HANDHELDS.uses))
class EntryCopy(unittest.TestCase):
def test_no_reference_label_on_home_explore_and_the_setup_index(self):
for name in ("index.md", "explore.md", "platforms/index.md"):
if not sitepages.page(name).is_file():
continue
body = _page(name).split("\n---\n", 1)[-1]
with self.subTest(page=name):
self.assertNotIn("Reference: [", body)
self.assertIn("- Have a file? [Check it](/retrobios/files/)", _page("index.md"))
def test_the_explore_cards_have_bare_links(self):
source = Path(generate_site.__file__).read_text(encoding="utf-8")
self.assertNotIn('"Reference: [How each platform checks files]', source)
self.assertNotIn('"Reference: [Data and API]', source)
def test_the_setup_index_lists_the_handheld_tiles(self):
text = _page("platforms/index.md")
for tile in siteguides.GROUP_TILES["handheld"]:
target = tile.link.removesuffix(".md").replace(".md#", "/#")
with self.subTest(tile=tile.name):
self.assertIn(f"**[{tile.name}](/retrobios/{target}", text.replace("/)", ")"))
self.assertIn(tile.text, text)
class IphoneAndIpad(unittest.TestCase):
def test_a_tile_in_the_handheld_group(self):
tile = next(t for t in siteguides.GROUP_TILES["handheld"] if t.name == "iPhone and iPad")
self.assertEqual(tile.link, "which-pack.md#iphone-and-ipad")
for name in ("index.md", "which-pack.md"):
with self.subTest(page=name):
self.assertIn('href="/retrobios/which-pack/#iphone-and-ipad"', _page(name))
self.assertEqual(sitemarkup.initials("iPhone and iPad"), "iP")
def test_each_app_step_is_cited_and_the_picker_replaces_the_zip(self):
text = _page("which-pack.md")
section = text.split("## iPhone and iPad { #iphone-and-ipad }", 1)[1].split("\n## ", 1)[0]
self.assertIn('data-pagefind-meta="devices:iPhone iPad iOS Delta Provenance"', section)
self.assertIn("/retrobios/which-pack/?platform=retroarch#only-some-consoles", section)
items = [line for line in section.splitlines() if line.startswith("- ")]
self.assertEqual([re.match(r"- \*\*(\w+)\*\*", i)[1] for i in items],
["RetroArch", "Delta", "Provenance"])
for item in items:
with self.subTest(item=item[:20]):
self.assertRegex(item, r"\(\[(docs|code)\]\(https://")
self.assertNotIn("/get/retroarch/", section)
class WhatItAsks(unittest.TestCase):
def test_the_prompt_block_is_framed_as_a_terminal(self):
self.assertIn('```text title="What it asks" frame="terminal"', _page("which-pack.md"))
if __name__ == "__main__":
unittest.main()