Files
libretro/tests/test_tool_messages.py
T
Abdessamad Derraz 4b54b82966 fix: suggest the target when no platform has it
verify --all --target swich ended on 'no platform has it' after seventeen INFO lines, two of which held the suggestion. The refusal now names the closest targets across every platform with the platforms that have them, and the per-platform lines print only when some platform keeps the target.
2026-11-05 19:21:23 +01:00

697 lines
31 KiB
Python

"""What the maintainer tools print: names a user can act on, counts that add up."""
from __future__ import annotations
import contextlib
import io
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import common # noqa: E402
import diff_truth # noqa: E402
import pipeline # noqa: E402
import truth # noqa: E402
import verify # noqa: E402
class ABuildbotNameSelectsItsProfile(unittest.TestCase):
"""`--emulator pcfx` was refused and pointed at two PlayStation cores,
though pcfx is in the cores: of beetle_pcfx."""
PROFILES = {
"beetle_pcfx": {"type": "libretro", "cores": ["pcfx"]},
"pcsx1": {"type": "libretro"},
"pcsx2": {"type": "standalone"},
"a": {"type": "libretro", "cores": ["shared"]},
"b": {"type": "libretro", "cores": ["shared"]},
}
def test_a_core_name_serves_the_profile(self):
selected = common.select_emulator_profiles(["pcfx"], self.PROFILES)
self.assertEqual([name for name, _profile in selected], ["beetle_pcfx"])
def test_a_name_two_profiles_claim_names_both(self):
with self.assertRaises(common.ProfileSelectionError) as caught:
common.select_emulator_profiles(["shared"], self.PROFILES)
self.assertIn("a, b", str(caught.exception))
def test_an_unknown_name_still_gets_a_hint(self):
with self.assertRaises(common.ProfileSelectionError) as caught:
common.select_emulator_profiles(["pcsx3"], self.PROFILES)
self.assertIn("Did you mean", str(caught.exception))
def test_cross_reference_reports_the_resolved_profile(self):
"""cross_reference filtered on the typed name, found no profile under
it and answered `pcfx is not run by None`."""
def report(name: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, "scripts/cross_reference.py", "--emulator", name],
cwd=REPO_ROOT, capture_output=True, text=True, timeout=600,
)
by_core, by_profile = report("pcfx"), report("beetle_pcfx")
self.assertEqual(by_core.returncode, 0, by_core.stderr)
self.assertEqual(by_core.stdout, by_profile.stdout)
class AMistypedTargetGetsTheClosestName(unittest.TestCase):
"""`--target swich` printed every target and alias on two lines."""
def test_did_you_mean(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
targets = Path(tmp) / "targets"
targets.mkdir()
names = [f"board-{n}" for n in range(30)] + ["nintendo-switch"]
(targets / "p.yml").write_text(
"targets:\n" + "".join(f" {n}:\n cores: [a]\n" for n in names)
)
(targets / "_overrides.yml").write_text(
"p:\n targets:\n nintendo-switch:\n aliases: [switch]\n"
)
with self.assertRaises(ValueError) as caught:
common.load_target_config("p", "swich", tmp)
message = str(caught.exception)
self.assertIn("Did you mean: switch?", message)
self.assertIn("lists all 31", message)
self.assertNotIn("board-0", message)
class AMistypedTargetAcrossAllPlatforms(unittest.TestCase):
"""`verify --all --target swich` ended on "no platform has it" after 17
INFO lines, two of which held the suggestion."""
def test_the_refusal_suggests_and_names_the_platforms(self):
platforms = common.list_registered_platforms(str(REPO_ROOT / "platforms"))
out = io.StringIO()
with contextlib.redirect_stdout(out), self.assertRaises(ValueError) as caught:
common.build_target_cores_cache(
platforms, "swich", str(REPO_ROOT / "platforms"), is_all=True
)
self.assertIn("Did you mean: switch (", str(caught.exception))
self.assertIn("retroarch", str(caught.exception))
self.assertEqual(out.getvalue(), "")
class AMistypedDataKeyGetsTheClosestName(unittest.TestCase):
"""`--key dolphin-sis` printed all 27 keys on one line."""
def test_did_you_mean(self):
proc = subprocess.run(
[sys.executable, "scripts/refresh_data_dirs.py", "--key", "dolphin-sis", "--dry-run"],
cwd=REPO_ROOT, capture_output=True, text=True, check=False,
)
self.assertEqual(proc.returncode, 1)
self.assertIn("unknown key: dolphin-sis. Did you mean: dolphin-sys? --dry-run lists all",
proc.stderr)
self.assertNotIn("ppsspp-assets", proc.stderr)
class TheGapHeadingAddsUp(unittest.TestCase):
"""Amiberry read 261 files in profile, 7 declared and 238 undeclared:
16 entries were set aside without a word."""
def test_every_entry_is_counted_once(self):
import cross_reference as xref
files = [
{"name": "kick.rom", "required": True},
{"name": "game.grp", "size": 10},
{"name": "game.grp", "size": 20},
{"name": "a.bin", "archive": "set.zip"},
{"name": "b.bin", "archive": "set.zip"},
{"name": "picked.bin", "path": None},
{"name": "gone.bin", "unsourceable": "never distributed"},
]
profiles = {"emu": {"emulator": "Emu", "type": "libretro", "systems": ["s"],
"files": files}}
report = xref.cross_reference(profiles, {}, {"files": {}, "indexes": {}},
all_declared={"kick.rom": [(None, "kick.rom")]})
data = report["emu"]
self.assertEqual(
xref.profile_breakdown(data),
"1 declared by platforms, 3 undeclared, 1 unsourceable, "
"2 set aside (1 other versions of a slot, 1 outside the pack)",
)
self.assertEqual(
data["declared_files"] + data["undeclared_files"] + data["unsourceable_files"]
+ sum(data["set_aside"].values()),
data["total_files"],
)
class AnOfflineRunWithNoCacheStopsFirst(unittest.TestCase):
"""On a fresh clone, pipeline.py --offline built every pack before saying
the data directories had never been fetched."""
def _args(self, **flags):
import argparse
return argparse.Namespace(**{"offline": True, "skip_packs": False, **flags})
def test_refused_before_any_step(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
cache = Path(tmp) / "cache"
(Path(tmp) / "_data_dirs.yml").write_text(
f"data_directories:\n a:\n local_cache: {cache}\n"
)
parser = mock.Mock()
pipeline._refuse_offline_without_caches(parser, self._args(), tmp)
self.assertIn("refresh_data_dirs.py", parser.error.call_args.args[0])
for args in (self._args(offline=False), self._args(skip_packs=True)):
parser = mock.Mock()
pipeline._refuse_offline_without_caches(parser, args, tmp)
parser.error.assert_not_called()
cache.mkdir()
parser = mock.Mock()
pipeline._refuse_offline_without_caches(parser, self._args(), tmp)
parser.error.assert_not_called()
class TheTruthDiffShowsWhereAndAsksNothing(unittest.TestCase):
"""The markdown wrote `**Remove** hiscore.dat (phantom)` beside six
`**Add** hiscore.dat`, none with a path, for files profiled cores read."""
def _report(self) -> dict:
truth_sys = {"files": [
{"name": "hiscore.dat", "path": "mame2003/hiscore.dat",
"md5": "a" * 32, "_cores": ["mame2003"]},
], "_coverage": {"cores_unprofiled": []}}
scraped_sys = {"files": [
{"name": "hiscore.dat", "destination": "fbneo/hiscore.dat", "md5": "b" * 32},
]}
report = truth.diff_platform_truth(
{"systems": {"arcade": truth_sys}}, {"systems": {"arcade": scraped_sys}}
)
report["platform"] = "p"
return report
def test_entries_carry_their_destination(self):
div = self._report()["divergences"]["arcade"]
self.assertEqual(div["extra_phantom"][0]["destination"], "fbneo/hiscore.dat")
self.assertEqual(div["missing"][0]["destination"], "mame2003/hiscore.dat")
def test_neither_format_asks_for_a_removal(self):
report = self._report()
for text in (diff_truth._format_markdown(report),
diff_truth._format_terminal(report)):
self.assertNotIn("Remove", text)
self.assertNotIn("phantom", text.lower())
self.assertIn("fbneo/hiscore.dat", text)
self.assertIn("mame2003/hiscore.dat", text)
class TheVerifySummaryCountsDiscrepancies(unittest.TestCase):
"""19 DISCREPANCY lines above a last line that read clean."""
def test_the_closing_line_names_them(self):
details = [
{"system": "arcade", "name": "aes.zip", "status": verify.Status.OK,
"discrepancy": "file present (OK) but hash differs"},
# Printed as MISSING, so not a second time as a discrepancy.
{"system": "arcade", "name": "x.zip", "status": verify.Status.MISSING,
"discrepancy": "unmet"},
{"system": "arcade", "name": "x.zip", "status": verify.Status.MISSING},
]
summary = verify._overall_summary([
({"severity_counts": {}, "details": details}, ["RetroArch"]),
({"severity_counts": {}, "details": []}, ["Batocera"]),
])
self.assertIn("1 discrepancy (RetroArch 1)", summary)
def test_a_shared_destination_names_the_path_and_both_sides(self):
result = {"name": "disk.rom", "status": verify.Status.UNTESTED}
entry = {"name": "disk.rom", "md5": "793f8678" + "0" * 24}
self.assertTrue(verify._twin_unmet(
result, "coco", entry, "nec-pc-88", "bios/disk.rom"
))
message = result["discrepancy"]
for part in ("bios/disk.rom", "nec-pc-88", "coco", "md5 793f8678"):
self.assertIn(part, message)
class EveryPlatformInTheSummaryCarriesItsCount(unittest.TestCase):
"""'17 discrepancies (Lakka 5, RetroArch 5, RetroBat 2, RetroDECK 4,
ROCKNIX)': the one platform with a single entry had no number, and the
named counts did not add up to the total."""
def test_a_count_of_one_is_written(self):
one = {"severity_counts": {verify.Severity.WARNING: 1}, "details": []}
two = {"severity_counts": {verify.Severity.WARNING: 2}, "details": []}
summary = verify._overall_summary([(two, ["Lakka"]), (one, ["ROCKNIX"])])
self.assertIn("3 warning (Lakka 2, ROCKNIX 1)", summary)
class HeldButNotPackedNamesEachReason(unittest.TestCase):
"""'Core files held but not packed: 16 (a packed file occupies its
directory)' on Lakka, where 14 of the 16 are installed with their
emulator: the first entry's reason stood for the whole count."""
@staticmethod
def _entry(name: str, reason: str, category: str = "bios") -> dict:
return {"name": name, "emulator": "x", "required": True, "in_repo": True,
"in_pack": False, "omitted": reason, "category": category}
def _printed(self, entries: list[dict]) -> str:
out = io.StringIO()
with contextlib.redirect_stdout(out):
verify._print_undeclared_section({"undeclared_files": entries}, False)
return out.getvalue()
def test_each_reason_carries_its_count(self):
entries = [self._entry("a.rom", "a packed file occupies its directory")]
entries += [self._entry(f"{i}.rom", "installed with its emulator") for i in range(3)]
entries += [self._entry(f"g{i}", "installed with its emulator", "game_data")
for i in range(2)]
entries.append(self._entry("g9", "no platform slug for its system", "game_data"))
text = self._printed(entries)
self.assertIn(
"Core files held but not packed: 4 (3 installed with its emulator, "
"1 a packed file occupies its directory)", text)
self.assertIn(
"Game data held but not packed: 3 (2 installed with its emulator, "
"1 no platform slug for its system)", text)
def test_a_missing_file_follows_the_line_that_counts_it(self):
"""Indented under 'held but not packed: 102 (installed with its
emulator)', Batocera's one missing sfiii2a.zip read as one of them."""
missing = {"name": "sfiii2a.zip", "emulator": "FinalBurn Neo", "required": True,
"in_repo": False, "category": "bios"}
lines = self._printed(
[missing, self._entry("a.rom", "installed with its emulator")]
).splitlines()
self.assertTrue(lines[0].startswith(" Core files: "), lines)
self.assertIn("MISSING (required): FinalBurn Neo needs sfiii2a.zip", lines[1])
self.assertTrue(lines[2].startswith(" Core files held but not packed: "), lines)
def test_one_name_read_at_two_paths_prints_both_paths(self):
"""MyMinUI printed 'FinalBurn Neo needs sfiii2a.zip' twice, word for
word, for Bios/FBN/ and Bios/NG/."""
base = {"name": "sfiii2a.zip", "emulator": "FinalBurn Neo", "required": True,
"in_repo": False, "category": "bios"}
lone = dict(base, name="other.zip", path="Bios/FBN/other.zip")
text = self._printed([dict(base, path="Bios/FBN/sfiii2a.zip"),
dict(base, path="Bios/NG/sfiii2a.zip"), lone])
self.assertIn("FinalBurn Neo needs Bios/FBN/sfiii2a.zip", text)
self.assertIn("FinalBurn Neo needs Bios/NG/sfiii2a.zip", text)
self.assertIn("FinalBurn Neo needs other.zip\n", text)
def test_a_single_reason_reads_as_before(self):
text = self._printed([self._entry("a.rom", "installed with its emulator")] * 2)
self.assertIn("Core files held but not packed: 2 (installed with its emulator)", text)
class APlatformWithoutTargetsSaysSo(unittest.TestCase):
"""`--platform misterfpga --target switch` printed 'No target config for
platform 'misterfpga': platforms/targets/misterfpga.yml', which reads as
a file missing from the repository."""
def test_the_message_names_the_reason_and_the_platforms_with_targets(self):
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / "targets").mkdir()
for name in ("retroarch", "batocera", "_overrides"):
(Path(tmp) / "targets" / f"{name}.yml").write_text("targets: {}\n")
with self.assertRaises(FileNotFoundError) as caught:
common.load_target_config("misterfpga", "switch", tmp)
self.assertEqual(
str(caught.exception),
"misterfpga has no hardware targets: drop --target "
"(platforms with targets: batocera, retroarch)",
)
class OneNameListsThePlatforms(unittest.TestCase):
"""install.py --list-platforms, generate_pack.py --list, and no listing
at all in verify.py: three scripts, three answers to one question."""
def _run(self, script: str, flag: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, f"scripts/{script}", flag], capture_output=True, text=True,
cwd=str(REPO_ROOT), timeout=120, check=False,
)
def test_both_tools_take_it_and_agree(self):
verify_out = self._run("verify.py", "--list-platforms")
pack_out = self._run("generate_pack.py", "--list-platforms")
self.assertEqual(verify_out.returncode, 0, verify_out.stderr)
self.assertEqual(pack_out.returncode, 0, pack_out.stderr)
self.assertEqual(verify_out.stdout, pack_out.stdout)
self.assertIn("retroarch", verify_out.stdout.split())
self.assertIn("--list-platforms", (REPO_ROOT / "install.py").read_text())
def test_the_installer_name_is_the_one_documented(self):
tools = (REPO_ROOT / "wiki" / "tools.md").read_text()
self.assertIn("generate_pack.py --list-platforms", tools)
self.assertIn("verify.py --list-platforms", tools)
class OneSpellingPerOption(unittest.TestCase):
"""verify.py -p batocera worked and slots.py -p batocera was refused."""
SHORT = {"--platform": "-p", "--target": "-t"}
# migrate.py's --target is the directory files move to, not a hardware target.
OTHER_MEANING = {("migrate.py", "--target")}
def test_every_script_takes_the_short_forms(self):
import ast # noqa: PLC0415
missing = []
for path in sorted((REPO_ROOT / "scripts").rglob("*.py")):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not (isinstance(node, ast.Call)
and getattr(node.func, "attr", None) == "add_argument"):
continue
flags = [a.value for a in node.args
if isinstance(a, ast.Constant) and isinstance(a.value, str)]
for long, short in self.SHORT.items():
if (path.name, long) in self.OTHER_MEANING:
continue
if long in flags and short not in flags:
missing.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno} {long}")
self.assertEqual(missing, [])
class ThePipelineRefreshesTheCommittedExport(unittest.TestCase):
"""Step 2e wrote to <output-dir>/upstream while exports/ is the copy the
site and the platforms read, so a green run left it stale."""
def test_the_export_step_writes_the_default_directory(self):
import ast
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
for node in ast.walk(ast.parse(source)):
if (isinstance(node, ast.Assign)
and any(getattr(t, "id", "") == "export_cmd" for t in node.targets)):
words = [getattr(e, "value", None) for e in node.value.elts]
self.assertIn("scripts/export_native.py", words)
self.assertNotIn("--output-dir", words)
return
self.fail("no export_cmd in pipeline.py")
class ThePipelineCountsItsRealSteps(unittest.TestCase):
"""Labels read N/8 for a run of 24 steps, and the summary used keys
the log never printed."""
def test_every_recorded_step_has_a_label(self):
import re # noqa: PLC0415
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
keys = set(re.findall(r'results\["(\w+)"\] =', source))
self.assertEqual(keys - set(pipeline.STEP_LABELS), set())
def test_a_label_written_out_is_one_of_the_list(self):
import re # noqa: PLC0415
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
written = re.findall(r'run\([^()]*?, "([^"]+)"\)', source)
numbers = {label.split(" ", 1)[0] for label in pipeline.STEP_LABELS.values()}
for label in written:
with self.subTest(label=label):
# A step's own label, or a second command of a listed step.
self.assertIn(label.split(" ", 1)[0], numbers)
def test_no_title_counts_eight_steps(self):
"""Three print() headers of step 5 still read "5/8" beside the
computed "5 consistency check [20/24]"."""
import re # noqa: PLC0415
source = (REPO_ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
self.assertEqual(re.findall(r'"[^"\n]*---[^"\n]*\b\d+[a-z]?/\d+\b[^"\n]*"', source), [])
def test_progress_comes_from_the_list(self):
total = len(pipeline.STEP_LABELS)
title = pipeline.step_title(pipeline.STEP_LABELS["render_site"])
self.assertEqual(title, f"8b render site [{total}/{total}]")
self.assertNotIn("/8", pipeline.step_title(pipeline.STEP_LABELS["verify"]))
def test_a_skipped_schema_check_says_how_to_run_it(self):
"""Step 0 said only "jsonschema not installed", then the run read
COMPLETE and the schema failed in PR validation."""
argv = ["pipeline.py", "--offline", "--skip-packs", "--skip-docs"]
out = io.StringIO()
real_find_spec = pipeline.importlib.util.find_spec
with mock.patch.object(sys, "argv", argv), mock.patch.object(
pipeline, "run", return_value=(True, "")
), mock.patch.object(
pipeline.importlib.util, "find_spec",
side_effect=lambda name, *a: None if name == "jsonschema"
else real_find_spec(name, *a),
), contextlib.redirect_stdout(out), contextlib.suppress(SystemExit):
pipeline.main()
line = next(
row for row in out.getvalue().splitlines()
if "0 validate schemas" in row and "SKIPPED" in row
)
self.assertIn('pip install "jsonschema[format-nongpl]"', line)
self.assertIn("PR validation runs this check", line)
def test_an_offline_buildbot_check_has_a_summary_row(self):
argv = ["pipeline.py", "--offline", "--check-buildbot", "--skip-packs",
"--skip-docs"]
out = io.StringIO()
with mock.patch.object(sys, "argv", argv), mock.patch.object(
pipeline, "run", return_value=(True, "RetroArch: 1/1 OK\n")
), contextlib.redirect_stdout(out), contextlib.suppress(SystemExit):
pipeline.main()
summary = out.getvalue().split("=" * 60)[1]
self.assertIn("2b check_buildbot", summary)
# The number the run printed for the step leads its summary row.
self.assertIn("4d bundled_only", summary)
self.assertTrue(
pipeline.step_title(pipeline.STEP_LABELS["bundled_only"]).startswith("4d ")
)
class AHashConflictNamesBothCores(unittest.TestCase):
"""The warning named the second core only, and not which value stayed."""
def test_both_cores_and_the_kept_value(self):
truth._CONFLICTS_SHOWN.clear()
system: dict = {}
err = io.StringIO()
with contextlib.redirect_stderr(err):
truth._merge_file_into_system(
system, {"name": "G.yaml", "path": "r/G.yaml", "sha1": "5e" * 20},
"pcsx2", None,
)
truth._merge_file_into_system(
system, {"name": "G.yaml", "path": "r/G.yaml", "sha1": "7e" * 20},
"yaps2", None,
)
self.assertIn(f"{'5e' * 20} (pcsx2) kept", err.getvalue())
self.assertIn(f"{'7e' * 20} (yaps2)", err.getvalue())
def test_the_origin_is_not_written_to_the_model(self):
profile = {"type": "libretro", "systems": ["sony-playstation"],
"files": [{"name": "a.bin", "system": "sony-playstation",
"md5": "a" * 32}]}
model = truth.generate_platform_truth(
"p", {"cores": ["c"], "systems": {}}, {}, {"c": profile}
)
entries = [f for s in model["systems"].values() for f in s.get("files", [])]
self.assertTrue(entries)
self.assertFalse(any("_hash_by" in f for f in entries))
class HelpTextsWrapBetweenWords(unittest.TestCase):
def test_check_freshness_areas(self):
proc = subprocess.run(
[sys.executable, "scripts/check_freshness.py", "--help"],
capture_output=True, text=True, cwd=REPO_ROOT, check=False,
env={**os.environ, "COLUMNS": "80"}, timeout=60,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("coreinfo", " ".join(proc.stdout.split()))
def test_docstring_descriptions_keep_their_layout(self):
"""romset_recipes ran its two questions into one paragraph and both
help texts printed reST double backquotes."""
for script, line in (("romset_recipes.py", "--identify"),
("validate_site.py", None)):
with self.subTest(script=script):
proc = subprocess.run(
[sys.executable, f"scripts/{script}", "--help"],
capture_output=True, text=True, cwd=REPO_ROOT, check=False,
env={**os.environ, "COLUMNS": "80"}, timeout=60,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertNotIn("``", proc.stdout)
if line:
self.assertIn(line, proc.stdout.splitlines())
class ListPlatformsTakesTheSharedFlag(unittest.TestCase):
"""verify, generate_pack, slots and pipeline say --include-archived."""
def test_both_spellings(self):
for flag in ("--include-archived", "--all"):
with self.subTest(flag=flag):
env = {k: v for k, v in os.environ.items() if k != "GITHUB_OUTPUT"}
proc = subprocess.run(
[sys.executable, "scripts/list_platforms.py", flag],
capture_output=True, text=True, cwd=REPO_ROOT, check=False,
env=env, timeout=60,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("retropie", proc.stdout)
def test_the_pack_list_marks_archived_platforms(self):
"""generate_pack --list printed retropie like a platform with a
release pack."""
proc = subprocess.run(
[sys.executable, "scripts/generate_pack.py", "--list"],
capture_output=True, text=True, cwd=REPO_ROOT, check=False, timeout=120,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
lines = proc.stdout.splitlines()
self.assertIn("retropie (archived, installer only)", lines)
self.assertIn("knulli (installer only)", lines)
self.assertIn("retroarch", lines)
class NoScriptReadsTheTokenOnItsOwn(unittest.TestCase):
"""Nine scripts read GITHUB_TOKEN from the environment after
github_token() existed, and went anonymous when run on their own.
download.py is the user's downloader: it runs without the repository and
a user's gh session is not its business."""
READS = ('environ.get("GITHUB_TOKEN"', "environ['GITHUB_TOKEN']",
'environ["GITHUB_TOKEN"]', 'environ.get("GH_TOKEN"')
OWNERS = {"scripts/common.py", "scripts/download.py"}
def test_every_reader_asks_github_token(self):
for path in sorted((REPO_ROOT / "scripts").rglob("*.py")):
relative = path.relative_to(REPO_ROOT).as_posix()
if relative in self.OWNERS:
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if " = " in line and line.split(" = ")[0].strip().startswith("env["):
continue
with self.subTest(file=relative, line=number):
self.assertFalse(any(read in line for read in self.READS), line.strip())
class TheGitHubTokenComesFromGh(unittest.TestCase):
"""Only refresh_stale asked gh: profile_sync refused the run and the
scrapers went anonymous for a maintainer logged in to gh."""
def setUp(self):
common._gh_cli_token.cache_clear()
self.addCleanup(common._gh_cli_token.cache_clear)
env = {k: v for k, v in os.environ.items()
if k not in ("GITHUB_TOKEN", "GH_TOKEN", "RETROBIOS_OFFLINE")}
patcher = mock.patch.dict(os.environ, env, clear=True)
patcher.start()
self.addCleanup(patcher.stop)
@staticmethod
def _gh(token: str = "gho_fromgh", code: int = 0):
return mock.patch(
"subprocess.run",
return_value=subprocess.CompletedProcess(["gh"], code, token, ""),
)
def test_gh_answers_when_the_variable_is_unset(self):
with self._gh():
self.assertEqual(common.github_token(), "gho_fromgh")
# Subprocesses of the run inherit it.
self.assertEqual(os.environ.get("GITHUB_TOKEN"), "gho_fromgh")
def test_the_variable_wins(self):
os.environ["GITHUB_TOKEN"] = "set"
with self._gh() as run:
self.assertEqual(common.github_token(), "set")
run.assert_not_called()
def test_no_gh_means_anonymous(self):
with mock.patch("subprocess.run", side_effect=FileNotFoundError("gh")):
self.assertEqual(common.github_token(), "")
with self._gh("", 1):
common._gh_cli_token.cache_clear()
self.assertEqual(common.github_token(), "")
def test_offline_runs_never_ask(self):
os.environ["RETROBIOS_OFFLINE"] = "1"
with self._gh() as run:
self.assertEqual(common.github_token(), "")
run.assert_not_called()
def test_every_api_caller_reads_it(self):
import profile_sync # noqa: PLC0415
import upstream # noqa: PLC0415
from scraper import base_scraper # noqa: PLC0415
with self._gh():
self.assertEqual(
upstream._headers("https://api.github.com/repos/o/n")["Authorization"],
"token gho_fromgh",
)
os.environ.pop("GITHUB_TOKEN")
with self._gh():
self.assertEqual(base_scraper.github_headers()["Authorization"],
"Bearer gho_fromgh")
os.environ.pop("GITHUB_TOKEN")
with self._gh(), contextlib.redirect_stderr(io.StringIO()):
profile_sync._check_quota(606, offline=False)
def test_another_forge_never_triggers_gh(self):
import upstream # noqa: PLC0415
with self._gh() as run:
headers = upstream._headers("https://gitlab.com/api/v4/projects/1")
self.assertNotIn("Authorization", headers)
run.assert_not_called()
class CitedCommandsRunWhenPasted(unittest.TestCase):
"""check_freshness wrote "run python scripts/generate_truth.py" on one
line and "export_native.py --platform bizhawk writes it again" two lines
below: pasted from the repository root, the second is command not found.
A message that hands the reader a command spells the whole command."""
CITED = re.compile(r"(?:^|run |with |then |, )([a-z_]+\.py) --[a-z]")
def test_no_message_cites_a_bare_script(self):
import ast # noqa: PLC0415
offenders = []
for path in sorted((REPO_ROOT / "scripts").rglob("*.py")):
tree = ast.parse(path.read_text(encoding="utf-8"))
docs = {
id(node.body[0].value)
for node in ast.walk(tree)
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef,
ast.AsyncFunctionDef))
and node.body and isinstance(node.body[0], ast.Expr)
}
offenders += [
f"{path.relative_to(REPO_ROOT)}:{node.lineno} {node.value.strip()[:60]}"
for node in ast.walk(tree)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
and id(node) not in docs and self.CITED.search(node.value)
]
self.assertEqual(offenders, [])
def test_the_target_hint_is_a_whole_command(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
targets = Path(tmp) / "targets"
targets.mkdir()
(targets / "p.yml").write_text("targets:\n switch:\n cores: [a]\n")
with self.assertRaises(ValueError) as caught:
common.load_target_config("p", "swich", tmp)
self.assertIn("python scripts/verify.py --platform p --list-targets",
str(caught.exception))
if __name__ == "__main__":
unittest.main()