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:
Abdessamad Derraz committed 2026-10-18 07:29:16 +02:00
1 parent 23ed3e92ab
commit 3116a57617
6 files changed
+183 -7

No files matched your search

+3 -2
View File
@@ -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}")
+44 -4
View File
@@ -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__":
+27
View File
@@ -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()
+11
View File
@@ -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"])
+47
View File
@@ -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
View File
@@ -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: