mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-18 09:23:26 -05:00
docs: document every option the scripts accept
Fifty options appeared in no page. fileless_audit also gains --all, so its example sweeps instead of naming three settled profiles, and it and download.py --info now fail on an unknown name or an unreachable source.
This commit is contained in:
1 parent
23ed3e92ab
commit
3116a57617
6 files changed
+183
-7
No files matched your search
+3
-2
@@ -298,8 +298,9 @@ def show_info(platform: str, release: dict) -> None:
|
||||
"""Show pack information for a platform."""
|
||||
pack = find_pack(release, platform)
|
||||
if not pack:
|
||||
print(f"Platform '{platform}' not found in release")
|
||||
return
|
||||
print(f"Platform '{platform}' not found in release.")
|
||||
print("Available:", ", ".join(list_platforms(release)))
|
||||
sys.exit(1)
|
||||
|
||||
print(f" Platform: {pack.platform}")
|
||||
print(f" File: {pack.name}")
|
||||
|
||||
@@ -20,10 +20,12 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
import upstream
|
||||
from common import suggest_names
|
||||
from profile_sync import collect_citations, select_views, split_source_ref
|
||||
from safeparse import yaml_load
|
||||
|
||||
@@ -84,20 +86,52 @@ def audit(
|
||||
return hits
|
||||
|
||||
|
||||
def _settled_before_reading(document: dict) -> bool:
|
||||
"""A profile answered without its sources: files, an exclusion note, or
|
||||
a declared data directory (see audit)."""
|
||||
return bool(
|
||||
document.get("files")
|
||||
or str(document.get("exclusion_note") or "").strip()
|
||||
or document.get("data_directories")
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
|
||||
parser.add_argument("emulators", nargs="+")
|
||||
parser.add_argument("emulators", nargs="*", help="profiles to read")
|
||||
parser.add_argument(
|
||||
"--all", action="store_true",
|
||||
help="every profile that declares no files and has no answer yet",
|
||||
)
|
||||
parser.add_argument("--emulators-dir", default="emulators")
|
||||
parser.add_argument("--cache-dir", default=".cache")
|
||||
parser.add_argument("--offline", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.all == bool(args.emulators):
|
||||
parser.error("name profiles or pass --all, not both")
|
||||
|
||||
flagged = 0
|
||||
for name in args.emulators:
|
||||
known = sorted(path.stem for path in Path(args.emulators_dir).glob("*.yml"))
|
||||
unknown = [name for name in args.emulators if name not in known]
|
||||
if unknown:
|
||||
raise SystemExit(
|
||||
"ERROR: unknown profile " + ", ".join(unknown) + ". "
|
||||
+ suggest_names(unknown, known, "verify.py --list-emulators")
|
||||
)
|
||||
names = args.emulators
|
||||
if args.all:
|
||||
names = []
|
||||
for name in known:
|
||||
with open(Path(args.emulators_dir) / f"{name}.yml", encoding="utf-8") as handle:
|
||||
if not _settled_before_reading(yaml_load(handle) or {}):
|
||||
names.append(name)
|
||||
|
||||
flagged = unreachable = 0
|
||||
for name in names:
|
||||
try:
|
||||
hits = audit(name, args.emulators_dir, args.cache_dir, args.offline)
|
||||
except upstream.UpstreamError as exc:
|
||||
print(f"{name}: unreachable, {exc}", file=sys.stderr)
|
||||
unreachable += 1
|
||||
continue
|
||||
if hits is None or not hits:
|
||||
continue
|
||||
@@ -105,7 +139,13 @@ def main() -> None:
|
||||
print(f"{name}: declares no files, yet its source asks for a directory")
|
||||
for path, signal in hits:
|
||||
print(f" {path}: {signal}")
|
||||
raise SystemExit(1 if flagged else 0)
|
||||
print(
|
||||
f"{len(names)} profile(s) read, {flagged} to read again"
|
||||
+ (f", {unreachable} unreachable" if unreachable else ""),
|
||||
file=sys.stderr,
|
||||
)
|
||||
# An unreachable source is not a clean one.
|
||||
raise SystemExit(1 if flagged or unreachable else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4300,5 +4300,32 @@ class WikiProseHasNoTornSentence(unittest.TestCase):
|
||||
self.fail(f"{page.name}:{number} ends mid-sentence: {line.strip()!r}")
|
||||
|
||||
|
||||
|
||||
class EveryOptionIsDocumented(unittest.TestCase):
|
||||
"""profile_sync's --system, --stale-before and --ref, auto_fetch
|
||||
--create-issues, download.py --info and forty more were in no page: a
|
||||
maintainer ran --all and spent the API quota that --system would have
|
||||
saved. --help lists a flag; only a page says when to reach for it."""
|
||||
|
||||
def test_every_long_option_of_a_tracked_script_is_in_the_docs(self):
|
||||
tracked = subprocess.run(
|
||||
["git", "ls-files", "scripts"], capture_output=True, text=True,
|
||||
cwd=str(ROOT), check=True,
|
||||
).stdout.split()
|
||||
docs = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in [*sorted((ROOT / "wiki").glob("*.md")),
|
||||
ROOT / "README.md", ROOT / "CONTRIBUTING.md"]
|
||||
)
|
||||
option = re.compile(r'add_argument\(\s*(?:"-\w",\s*)?"(--[\w-]+)"')
|
||||
undocumented = sorted(
|
||||
f"{name}: {flag}"
|
||||
for name in tracked if name.endswith(".py")
|
||||
for flag in set(option.findall((ROOT / name).read_text(encoding="utf-8")))
|
||||
if not re.search(re.escape(flag) + r"(?![\w-])", docs)
|
||||
)
|
||||
self.assertEqual(undocumented, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,10 +9,12 @@ checksum of each layout and the staging location.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import hashlib
|
||||
import http.server
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -94,6 +96,15 @@ def _release(names, base="https://example.invalid", size=10):
|
||||
|
||||
|
||||
class TestPackGrouping(unittest.TestCase):
|
||||
def test_info_on_an_unknown_platform_fails(self):
|
||||
"""--info printed "not found" and exited 0."""
|
||||
release = _release(["EmuDeck_2.3.8_BIOS_Pack.zip"])
|
||||
with contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
download.show_info("emudek", release)
|
||||
self.assertEqual(caught.exception.code, 1)
|
||||
self.assertIn("EmuDeck", out.getvalue())
|
||||
|
||||
def test_whole_pack_has_one_part(self):
|
||||
packs = download.group_packs(_release(["EmuDeck_2.3.8_BIOS_Pack.zip"]))
|
||||
self.assertEqual([p.name for p in packs], ["EmuDeck_2.3.8_BIOS_Pack.zip"])
|
||||
|
||||
@@ -99,6 +99,53 @@ class TestFilelessAudit(unittest.TestCase):
|
||||
self.assertEqual(len(self._run()), 1)
|
||||
|
||||
|
||||
class TestCommandLine(unittest.TestCase):
|
||||
"""Named profiles were the only mode, so the documented example printed
|
||||
nothing; a misspelt name ended in a traceback and an unreachable source
|
||||
left the run green."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
Path(self.tmp.name, "open.yml").write_text(BASE, encoding="utf-8")
|
||||
Path(self.tmp.name, "done.yml").write_text(
|
||||
"emulator: D\nfiles:\n - name: a.bin\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
def _main(self, *argv, audit=None):
|
||||
read = []
|
||||
|
||||
def fake(name, *args):
|
||||
read.append(name)
|
||||
if audit:
|
||||
return audit(name)
|
||||
return []
|
||||
|
||||
with mock.patch.object(sys, "argv", ["fileless_audit.py", *argv,
|
||||
"--emulators-dir", self.tmp.name]), \
|
||||
mock.patch.object(fileless_audit, "audit", side_effect=fake), \
|
||||
mock.patch("sys.stderr"), mock.patch("sys.stdout"):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
fileless_audit.main()
|
||||
return caught.exception.code, read
|
||||
|
||||
def test_all_reads_only_the_profiles_nobody_answered(self):
|
||||
code, read = self._main("--all")
|
||||
self.assertEqual((code, read), (0, ["open"]))
|
||||
|
||||
def test_a_misspelt_profile_is_named(self):
|
||||
code, read = self._main("opne")
|
||||
self.assertIn("open", str(code))
|
||||
self.assertEqual(read, [])
|
||||
|
||||
def test_an_unreachable_source_fails_the_run(self):
|
||||
def unreachable(name):
|
||||
raise fileless_audit.upstream.UpstreamError("HTTP 403")
|
||||
|
||||
code, _ = self._main("open", audit=unreachable)
|
||||
self.assertEqual(code, 1)
|
||||
|
||||
|
||||
class TestCorpusIsAnswered(unittest.TestCase):
|
||||
"""Every fileless profile is covered, answered, or flagged.
|
||||
|
||||
|
||||
+51
-1
@@ -279,9 +279,14 @@ Names the profiles that declare no files whose source asks for a directory to
|
||||
read from.
|
||||
|
||||
```bash
|
||||
python scripts/fileless_audit.py craft dice lutro
|
||||
python scripts/fileless_audit.py --all # every profile not yet answered
|
||||
python scripts/fileless_audit.py virtualjaguar b2 # the profiles named
|
||||
```
|
||||
|
||||
It exits 1 when a profile is worth a reading or a source could not be reached
|
||||
(`--offline` reads the cache only, `--cache-dir` moves it), and ends on a
|
||||
count of what it read.
|
||||
|
||||
An empty `files:` list is the one assertion in the repository that ages
|
||||
unwatched: there is no file to go missing and no ref to drift, so nothing
|
||||
notices when a core that embedded everything grows a path. virtualjaguar
|
||||
@@ -343,6 +348,21 @@ python scripts/profile_sync.py --all --changed-only --json
|
||||
python scripts/profile_sync.py --emulator vice --fetch-plan
|
||||
```
|
||||
|
||||
Selection narrows `--all` before any request is spent: `--system sega-saturn`,
|
||||
`--type standalone`, `--stale-before 2026-06-01` (profiled before that date)
|
||||
and `--limit 20`. Output takes `--markdown` (a report written under
|
||||
`--report-dir`, `reports/` by default), and `--full-diff` takes `--ref PATH`
|
||||
to show one cited file and `--context N` lines around each hunk. `--strict`
|
||||
exits 1 when a profile has refs to review; CI runs it on every profile a change
|
||||
adds. `--offline` answers from `--cache-dir` alone.
|
||||
|
||||
```bash
|
||||
python scripts/profile_sync.py --all --system sega-saturn --changed-only
|
||||
python scripts/profile_sync.py --all --stale-before 2026-06-01 --limit 20 --triage
|
||||
python scripts/profile_sync.py --emulator vice --full-diff --ref vice/src/c64/c64rom.h --context 6
|
||||
python scripts/profile_sync.py --emulator vice --strict
|
||||
```
|
||||
|
||||
Per part of a ref: `ANCHORED` (same content, same lines), `SHIFTED` (same
|
||||
content, moved), `RENAMED` (the source file moved), `CHANGED` (content
|
||||
edited), `AMBIGUOUS` (several equally good candidates), `GONE` (nothing
|
||||
@@ -568,6 +588,36 @@ python scripts/refresh_stale.py --only platforms,targets --jobs 6
|
||||
| `refresh_stale.py` | Run the refresher behind every stale row of that report (see above) |
|
||||
| `migrate.py` | Migrate flat bios structure to Manufacturer/Console/ hierarchy |
|
||||
|
||||
Further options, by script:
|
||||
|
||||
| Script | Options |
|
||||
|--------|---------|
|
||||
| `auto_fetch.py` | `--create-issues` prints a GitHub issue body per file still missing; it posts nothing |
|
||||
| `check_buildbot_system.py` | `--update` refreshes the entries found stale, `--json`, `--registry` |
|
||||
| `check_freshness.py` | `--cache-dir`, `--native-cache-dir` (originals of the native export) and `--truth-dir` move the caches it reads |
|
||||
| `check_release.py` | `--tag`, `--sums dist/SHA256SUMS.txt`, `--record` (default `release.json`), `--json` |
|
||||
| `check_release_assets.py` | `--gitignore` (the asset registry), `--db`, `--notes FILE`, `--json` |
|
||||
| `diff_truth.py` | `--format markdown`, `--truth-dir`, `--target` |
|
||||
| `download.py` | `--list`, `--info <platform>` shows a pack's file name, size and parts without downloading |
|
||||
| `export_native.py` | `--truth-dir`, `--upstream-dir` (where the platforms' own files are cached) |
|
||||
| `find_pinned_revision.py` | `--repo URL --path FILE --md5 HASH`, `--write PATH`, `--cache-dir` |
|
||||
| `generate_db.py` | `--provenance-dir` (dump-catalog snapshots joined into the database) |
|
||||
| `generate_site.py` | `--docs-dir` (output, `docs/` by default) |
|
||||
| `mame_ref_audit.py` | `--write` points each stale ref at the line declaring its set, `--offline`, `--cache-dir` |
|
||||
| `migrate.py` | `--dry-run`, `--include-branches` also reads BIOS files from every remote branch |
|
||||
| `provenance_report.py` | `--provenance-dir`, `--write-derivable PATH` (with `--derive`; the pipeline writes `derivable.json`) |
|
||||
| `refresh_stale.py` | `--freshness-arg FLAG` passes a flag through to `check_freshness.py`, repeatable |
|
||||
| `release_record.py` | `--install-dir` (manifests the archives are held against), `--output` |
|
||||
| `restore_large_files.py` | `--cache` (downloaded assets), `--gitignore`, `--db` |
|
||||
| `romset_recipes.py` | `--dat-recipes PATH`, a snapshot or a directory of them (default `recipes/`) |
|
||||
| `split_pack.py` | `--max-size 1900M`, the ceiling per part (K, M, G; default 2G) |
|
||||
| `torrentzip.py` | `--check ZIP` reports conformance, `--rebuild ZIP -o OUT` normalizes an archive |
|
||||
| `validate_pr.py` | `--changed` takes the BIOS files the branch changes, `--markdown`, `--json` |
|
||||
| `validate_site.py` | `--site-dir` (rendered site), `--config` (mkdocs.yml), `--max-errors N` |
|
||||
| `scraper/coreinfo_scraper.py` | `--compare-db database.json` lists the `.info` firmware the database lacks |
|
||||
| `scraper/mame_hash_scraper.py` | `--release mameNNNN --profile FILE --source-commit SHA --core NAME --built-by PLATFORM` writes the profile of a MAME build a platform compiles |
|
||||
| `scraper/romset_dat_importer.py` | `--cache-dir` (downloaded listxml and DATs) |
|
||||
|
||||
## Installation tools
|
||||
|
||||
Cross-platform BIOS installer for end users:
|
||||
|
||||
Reference in new issue
Block a user