mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
chore: guard the site against personal pronouns
This commit is contained in:
1 parent
a70f8bcb34
commit
78e22851dd
2 files changed
+208
-4
No files matched your search
@@ -0,0 +1,204 @@
|
||||
"""The site speaks without personal pronouns.
|
||||
|
||||
Every page is written as an instruction or a plain statement: "Open the
|
||||
Start menu.", "The installer finds the BIOS folder." No I, we, you or their
|
||||
forms in the wiki sources, the pages the generators write, the README and
|
||||
CONTRIBUTING, llms.txt or the agent skill. Code, URLs, markup and anchor ids
|
||||
are not prose.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
||||
|
||||
import html
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS = ROOT / "website" / "src" / "content" / "docs"
|
||||
|
||||
PRONOUN = re.compile(
|
||||
r"(?<![\w-])((?-i:I)|me|my|mine|myself|we|us|our|ours|ourselves"
|
||||
r"|you|your|yours|yourself|yourselves)(?![\w-])",
|
||||
re.I,
|
||||
)
|
||||
|
||||
FENCED = re.compile(r"^[ \t]*(```|~~~).*?^[ \t]*\1[^\n]*$", re.S | re.M)
|
||||
INLINE_CODE = re.compile(r"(`+)(?:(?!\n\n)[^`])*?\1")
|
||||
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
|
||||
SCRIPT_STYLE = re.compile(r"<(script|style|pre|code)\b.*?</\1>", re.S | re.I)
|
||||
TAG = re.compile(r"</?[A-Za-z][^<>]*>")
|
||||
ENTITY = re.compile(r"&#(?:\d+|x[0-9a-fA-F]+);")
|
||||
URL = re.compile(r"(?:https?|ftp|smb)://\S+|\bwww\.\S+")
|
||||
LINK_TARGET = re.compile(r"\]\([^)\s]*(?:\s+\"[^\"]*\")?\)|^\[[^\]]+\]:\s*\S+.*$", re.M)
|
||||
ANCHOR = re.compile(r"\{[^{}\n]*#[^{}\n]*\}")
|
||||
PATHLIKE = re.compile(r"\S*[/\\]\S*")
|
||||
# File names, domains and dotted initials: baserom.us.z64, delete.me, A.I.R.
|
||||
DOTTED = re.compile(r"[\w-]+(?:\.[\w-]+)+\.?")
|
||||
# A list of single letters or digits (D, F, I, P), or of lowercase option
|
||||
# values in parentheses (euro, us, asia), names keys, not people.
|
||||
LETTERS = re.compile(r"(?<!\w)[A-Z0-9](?:,\s?[A-Z0-9#@^](?!\w))+")
|
||||
VALUES = re.compile(r"\((?:[a-z][\w-]*, )+[a-z][\w-]*\)")
|
||||
LETTER_HEADING = re.compile(r"^#+ [A-Z]\s*$", re.M)
|
||||
# Search keywords, written the way readers type a query. Pagefind matches
|
||||
# them, the page never shows them.
|
||||
TERMS = re.compile(r"^terms: .*$", re.M)
|
||||
# A capitalised "US" names the country and its region code.
|
||||
COUNTRY = re.compile(r"(?<![\w-])US(?![\w-])")
|
||||
|
||||
# Proper names that hold one of the words.
|
||||
NAMES: dict[str, str] = {
|
||||
r"I(?:'|''|'|')Max": "I'Max MACS, an arcade board maker",
|
||||
r"My (?:Old)?Boy!": "My Boy! and My OldBoy!, Android emulators",
|
||||
r"My Vision": "Nichibutsu My Vision, a console",
|
||||
r"On My iPhone": "the Files app location on iOS",
|
||||
r"Us vs Them": "a game title",
|
||||
r"Aliens Ate My Railway|Fell Apart On Me": "OpenTTD music track titles",
|
||||
r"\b(?:Model|Level|Series|Mk|MK|Ultima|Interface|Type) I\b(?!')":
|
||||
"a roman numeral in a machine or game name (Model I, Ultima I)",
|
||||
r"\bI(?= \([A-Z])": "a model letter, I (Palladium)",
|
||||
r"Java ME": "Java ME, the mobile Java platform",
|
||||
r"laseractive us(?=:)": "the LaserActive region key of geargrafx's BIOS list",
|
||||
}
|
||||
|
||||
# Program output quoted word for word. INSTALLER holds install.py's own
|
||||
# messages, which the pass over the installer's wording rewrites, and empties
|
||||
# this list with it. SOURCE holds what an emulator prints, quoted in its
|
||||
# profile from the emulator's code, which this project does not write.
|
||||
INSTALLER: dict[str, str] = {
|
||||
"Where do you play?": "title of the installer's first screen",
|
||||
"Copy the BIOS files from your old card or a folder": "installer menu row",
|
||||
"copy the BIOS files from your other card": "installer menu row on a two-card handheld",
|
||||
"if your files go there.": "installer hint for a card with no handheld system folder",
|
||||
"See [Files you already had]": (
|
||||
"link label in the generated options table of wiki/installer.md, "
|
||||
"kept by generate_installer_doc"),
|
||||
}
|
||||
SOURCE: dict[str, str] = {
|
||||
"You do not have a ROM image installed for Model": "sdltrs message, emulators/sdl2trs.yml",
|
||||
"You might not get sound because": "lindbergh-loader message, emulators/lindbergh-loader.yml",
|
||||
'"Your Card"': "KEGS disk name, emulators/kegs.yml",
|
||||
"How do I use SC3000": "MG.html question title, emulators/mastergear.yml",
|
||||
"How do I use SF7000 disk images?": "MG.html question title, emulators/mastergear.yml",
|
||||
"There is no version of MG for my Unix!": "MG.html question title, emulators/mastergear.yml",
|
||||
"rename your own SFX dat file to this name": "Zelda Classic README, emulators/zc210.yml",
|
||||
"Could not load ROM images! You must supply these.":
|
||||
"hypseus message, emulators/hypseus-singe.yml",
|
||||
"Please copy pak0.pk3 from the 'etmain' path of your":
|
||||
"ET: Legacy message, emulators/etlegacy.yml",
|
||||
'"keyconf.dat(us)"': "file name px68k ships, emulators/px68k-sdl.yml",
|
||||
}
|
||||
|
||||
_SPACED = [
|
||||
re.compile(r"\s+".join(map(re.escape, phrase.split())))
|
||||
for phrase in (*INSTALLER, *SOURCE)
|
||||
]
|
||||
_NAMES = [re.compile(pattern) for pattern in NAMES]
|
||||
|
||||
|
||||
def _blank(match: re.Match[str]) -> str:
|
||||
return re.sub(r"[^\n]", " ", match[0])
|
||||
|
||||
|
||||
def prose(text: str) -> str:
|
||||
"""The text of a page with everything that is not prose blanked."""
|
||||
for pattern in (HTML_COMMENT, SCRIPT_STYLE, TAG):
|
||||
text = pattern.sub(_blank, text)
|
||||
# _ and ' spell a file name or a name: read them as characters.
|
||||
text = ENTITY.sub(lambda m: html.unescape(m[0]), text)
|
||||
for pattern in (
|
||||
*_SPACED, FENCED, INLINE_CODE, LINK_TARGET, URL, ANCHOR, PATHLIKE,
|
||||
TERMS, *_NAMES, DOTTED, LETTERS, VALUES, LETTER_HEADING, COUNTRY,
|
||||
):
|
||||
text = pattern.sub(_blank, text)
|
||||
return text
|
||||
|
||||
|
||||
def pronouns(text: str) -> list[tuple[int, str]]:
|
||||
"""(line number, line) for each prose line holding a personal pronoun."""
|
||||
lines = text.splitlines()
|
||||
return [
|
||||
(number, lines[number - 1].strip())
|
||||
for number, line in enumerate(prose(text).splitlines(), 1)
|
||||
if PRONOUN.search(line)
|
||||
]
|
||||
|
||||
|
||||
def _hits(paths: list[Path]) -> list[str]:
|
||||
return [
|
||||
f"{path.relative_to(ROOT)}:{number}: {line}"
|
||||
for path in paths
|
||||
for number, line in pronouns(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
|
||||
|
||||
@cache
|
||||
def _generated() -> bool:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "generate_site.py")],
|
||||
cwd=ROOT, capture_output=True, text=True, check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
class Scanner(unittest.TestCase):
|
||||
def test_code_urls_and_anchors_are_not_prose(self):
|
||||
text = (
|
||||
"Run `install.py --help` first.\n"
|
||||
"```\nyou are here\n```\n"
|
||||
"See https://example.org/your/page and [a link](our-page.md).\n"
|
||||
"## Get the files { #get-your-files }\n"
|
||||
'<a title="your title">Open the menu</a>\n'
|
||||
"A US BIOS, Quake II I mean.\n"
|
||||
)
|
||||
self.assertEqual(pronouns(text), [(8, "A US BIOS, Quake II I mean.")])
|
||||
|
||||
def test_each_form_is_found_in_any_case(self):
|
||||
for word in ("I", "me", "My", "mine", "we", "Us", "OUR", "you", "Yours",
|
||||
"yourselves", "you're", "I'm"):
|
||||
with self.subTest(word=word):
|
||||
self.assertTrue(pronouns(f"Then {word} copy it."))
|
||||
|
||||
def test_a_word_holding_the_letters_passes(self):
|
||||
self.assertEqual(pronouns("Usage of the menu, mine-craft, youth."), [])
|
||||
|
||||
|
||||
class SitePronouns(unittest.TestCase):
|
||||
def test_wiki_sources(self):
|
||||
hits = _hits(sorted((ROOT / "wiki").glob("*.md")))
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_readme_and_contributing(self):
|
||||
hits = _hits([ROOT / "README.md", ROOT / "CONTRIBUTING.md"])
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_generated_pages(self):
|
||||
if not _generated():
|
||||
self.fail("scripts/generate_site.py failed")
|
||||
pages = sorted(p for p in DOCS.rglob("*") if p.suffix in (".md", ".mdx"))
|
||||
self.assertTrue(pages)
|
||||
hits = _hits(pages)
|
||||
self.assertEqual(hits, [], "\n".join(hits[:200]))
|
||||
|
||||
def test_agent_files(self):
|
||||
if not _generated():
|
||||
self.fail("scripts/generate_site.py failed")
|
||||
public = ROOT / "website" / "public"
|
||||
files = [public / "llms.txt", *sorted((public / "skills").rglob("*.md"))]
|
||||
self.assertTrue(all(path.is_file() for path in files))
|
||||
hits = _hits(files)
|
||||
self.assertEqual(hits, [], "\n".join(hits))
|
||||
|
||||
def test_every_allowed_quote_says_why(self):
|
||||
for quoted, reason in {**NAMES, **INSTALLER, **SOURCE}.items():
|
||||
with self.subTest(quoted=quoted):
|
||||
self.assertTrue(reason.strip())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -69,7 +69,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Pack probe | the cores a platform runs for a system, staging grown to a fixed point, runs mapped to pack and manifest entries, the line and its schema, the probe lock per run, that no page shows its lines. One class builds a real pack narrowed to PlayStation and lays it out both ways, from the archive and with `install.py --dest`, and skips without the collection | `test_pack_probe` |
|
||||
| Automation runner, disk cleanup, asset sync and bit-flip repair | fake jobs, git repositories with a bare origin and session worktrees, a committed file with one bit flipped, canned CI runs in place of `gh` | `test_autorun` |
|
||||
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_batocera_md5_list`, `test_check_freshness`, `test_cited_lines`, `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_dedup`, `test_doc_pages`, `test_doc_surfaces`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_optional_dependencies`, `test_pin_build`, `test_profile_sync`, `test_prose_style`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_retired_files`, `test_romset_recipes`, `test_schema_semantic_gate`, `test_scraper_contract`, `test_site_agents`, `test_site_arcade_boards`, `test_site_availability`, `test_site_bootstrap`, `test_site_built_gate`, `test_site_components`, `test_site_console_folders`, `test_site_content_data`, `test_site_copy`, `test_site_cores`, `test_site_css`, `test_site_demo`, `test_site_density_cards`, `test_site_emulator_page`, `test_site_emulators`, `test_site_entry`, `test_site_entry_tables`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_inputs`, `test_site_insights`, `test_site_integration`, `test_site_nav`, `test_site_picker`, `test_site_platform_density`, `test_site_platform_hashes`, `test_site_privacy`, `test_site_pronouns`, `test_site_reference`, `test_site_render`, `test_site_search`, `test_site_steps`, `test_site_system_links`, `test_site_system_names`, `test_site_system_sections`, `test_site_systems`, `test_site_validation`, `test_site_weight`, `test_site_wiki`, `test_site_write`, `test_source_ref_targets`, `test_upstream`, `test_upstream_index`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_version_basis`, `test_workflow_paths` |
|
||||
| Games on the card | small DATs, listxml and setup sources, a fake fetcher, fixture install lists, and the committed data against its schemas | `test_arcade_parents`, `test_game_catalogs`, `test_game_headers`, `test_game_regions`, `test_games_index`, `test_romfolders_card`, `test_romfolders_es` |
|
||||
|
||||
## Test architecture
|
||||
@@ -219,7 +219,7 @@ disambiguated by a hash suffix on purpose.
|
||||
## How to add a test
|
||||
|
||||
1. **Pick the right category.** Find the number range that matches the
|
||||
subsystem you are testing. If none fits, start a new range after the last
|
||||
subsystem under test. If none fits, start a new range after the last
|
||||
existing one.
|
||||
|
||||
2. **Create synthetic fixtures.** Write the minimum YAML configs and fake
|
||||
@@ -235,7 +235,7 @@ disambiguated by a hash suffix on purpose.
|
||||
method, file counts, or pack contents. Avoid brittle assertions on log
|
||||
output or formatting.
|
||||
|
||||
5. **Run the full suite.** After adding your test, run `python -m unittest
|
||||
5. **Run the full suite.** After adding the test, run `python -m unittest
|
||||
discover -s tests -t . -v` to verify nothing else broke.
|
||||
|
||||
A new module starts with `import tests` right after
|
||||
@@ -497,7 +497,7 @@ chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
Files are staged by name, never with `git add -A` or `git add .`, which sweep in
|
||||
deletions and work that is not yours. Generated artifacts (`database.json`,
|
||||
deletions and other people's work. Generated artifacts (`database.json`,
|
||||
`retired.json`, `derivable.json`, `install/*.json`, `README.md`, `CONTRIBUTING.md`, `exports/`,
|
||||
the generated tables and run samples of `wiki/installer.md`) are committed
|
||||
with the data they derive from, never alone: a manifest committed before the
|
||||
|
||||
Reference in new issue
Block a user