diff --git a/platforms/_guides.yml b/platforms/_guides.yml index c9eec77c..0cf62299 100644 --- a/platforms/_guides.yml +++ b/platforms/_guides.yml @@ -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: - >- diff --git a/scripts/generate_site.py b/scripts/generate_site.py index e9c10f66..11e4ce83 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -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 += ["", ""] + 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: "", "", "", + *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", diff --git a/scripts/siteguides.py b/scripts/siteguides.py index c8bbe383..59c98491 100644 --- a/scripts/siteguides.py +++ b/scripts/siteguides.py @@ -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) diff --git a/scripts/sitemarkup.py b/scripts/sitemarkup.py index f6ae72a8..a2a24f2d 100644 --- a/scripts/sitemarkup.py +++ b/scripts/sitemarkup.py @@ -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() diff --git a/tests/test_site_funnel.py b/tests/test_site_funnel.py index d9ff2b25..abf8523d 100644 --- a/tests/test_site_funnel.py +++ b/tests/test_site_funnel.py @@ -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)}") diff --git a/tests/test_site_r3_entry.py b/tests/test_site_r3_entry.py index 4c3fee39..20484abf 100644 --- a/tests/test_site_r3_entry.py +++ b/tests/test_site_r3_entry.py @@ -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" \(\d[\d.]* [KMG]B\) \|$") def test_the_zip_note(self): + text = re.sub(r"]*>", "", 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"]*>", "", _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): diff --git a/tests/test_site_r4_entry.py b/tests/test_site_r4_entry.py new file mode 100644 index 00000000..a0285496 --- /dev/null +++ b/tests/test_site_r4_entry.py @@ -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"]*>", "", 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()