mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
313 lines
15 KiB
Python
313 lines
15 KiB
Python
"""CONTRIBUTING.md and the site's contributing page say the same thing.
|
|
|
|
Two generators wrote the page and drifted: four steps to add a platform in
|
|
the repository, five on the site, under two different titles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
|
|
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
|
|
import generate_readme # noqa: E402
|
|
import generate_site # noqa: E402
|
|
|
|
|
|
def _normalized(text: str) -> str:
|
|
# The site ends each page with its next step in the reading order. That
|
|
# is navigation, not contributing text, and a repository file has none.
|
|
text = text.split("\n## Next steps\n", 1)[0].rstrip("\n")
|
|
text = re.sub(r"\]\([^)]*\)", "](link)", text)
|
|
return "\n".join(text.splitlines()[1:]).replace("on this site", "on the documentation site")
|
|
|
|
|
|
class OneContributingText(unittest.TestCase):
|
|
def test_repository_and_site_pages_match(self):
|
|
self.assertEqual(
|
|
_normalized(generate_readme.generate_contributing()),
|
|
_normalized(generate_site.generate_contributing()),
|
|
)
|
|
|
|
|
|
class CoverageReadsTheGivenProfiles(unittest.TestCase):
|
|
"""generate_site --emulators-dir changed the emulator pages but not the
|
|
platform coverage, which read ./emulators whatever was given."""
|
|
|
|
def test_the_directory_reaches_verify(self):
|
|
seen: list[str] = []
|
|
|
|
def fake_verify(_config, _db, emulators_dir, **_kwargs):
|
|
seen.append(emulators_dir)
|
|
return {"status_counts": {}, "total_files": 0, "undeclared_files": [], "details": []}
|
|
|
|
with mock.patch.object(generate_readme, "verify_platform", fake_verify), \
|
|
mock.patch.object(generate_readme, "load_emulator_profiles", return_value={}):
|
|
generate_readme.compute_coverage(
|
|
"retroarch", str(REPO_ROOT / "platforms"), {"files": {}}, emulators_dir="elsewhere"
|
|
)
|
|
self.assertEqual(seen, ["elsewhere"])
|
|
|
|
|
|
class BeforeOpeningAPullRequest(unittest.TestCase):
|
|
"""The section asked for the full pipeline: on a fresh fork, without
|
|
data/, it rebuilt 35 GB of packs and then failed on the data
|
|
directories, while CI asks for three checks."""
|
|
|
|
def test_it_lists_the_checks_ci_runs_and_not_the_pipeline(self):
|
|
import re as _re
|
|
|
|
text = (REPO_ROOT / "CONTRIBUTING.md").read_text(encoding="utf-8")
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
block = "".join(_re.findall(r"```bash\n(.*?)```", section, _re.S))
|
|
for command in ("validate_pr.py --changed", "validate_schemas.py --source-only",
|
|
"unittest discover"):
|
|
self.assertIn(command, block)
|
|
self.assertNotIn("pipeline.py", block)
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
self.assertIn("validate_schemas.py --source-only", workflow)
|
|
# CI holds added profiles to their upstream, not edited ones: --changed
|
|
# failed a contributor on refs they never wrote.
|
|
self.assertIn("profile_sync.py --added --base", workflow)
|
|
self.assertIn("profile_sync.py --added --strict", block)
|
|
self.assertNotIn("upstream/main", section)
|
|
self.assertNotIn("--changed --strict", block)
|
|
|
|
def test_it_names_the_site_build_and_the_packages_ci_runs(self):
|
|
"""A wiki-only change passed every listed check and failed CI on
|
|
validate_site.py, which the list did not name."""
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
for command in ("npm ci --prefix website", "npm run build --prefix website",
|
|
"validate_site.py --site-dir website/dist",
|
|
"vermin --no-tips -t=3.8- --violations install.py"):
|
|
with self.subTest(command=command):
|
|
self.assertIn(command, workflow)
|
|
self.assertIn(command, section)
|
|
installed = re.search(r'^\s*run: pip install (pyyaml "jsonschema.*markdown.*)$',
|
|
workflow, re.M).group(1)
|
|
pip_line = next(line for line in section.splitlines() if line.startswith("pip install"))
|
|
for package in (re.split(r"[\[=]", token.strip('"'))[0] for token in installed.split()):
|
|
with self.subTest(package=package):
|
|
self.assertRegex(pip_line, rf"\b{package}\b")
|
|
|
|
def test_a_file_too_large_for_git_has_a_route(self):
|
|
"""The guide sent any file over 25 MB through a clone, which GitHub
|
|
refuses past 100 MB, and only a maintainer can write to the release."""
|
|
import validate_pr
|
|
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## What happens next", 1)[1].split("\n## ", 1)[0]
|
|
self.assertEqual(validate_pr.MAX_FILE_SIZE, 100 * 1024 * 1024)
|
|
self.assertIn("100 MB", section)
|
|
self.assertIn(f"]({validate_pr.LARGE_FILE_ISSUE_URL})", section)
|
|
|
|
def test_it_installs_what_the_checks_need_and_no_site_package(self):
|
|
"""The site is built by Node from website/package-lock.json: a pip
|
|
line naming the old theme installed packages nothing reads."""
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
self.assertIn('pip install pyyaml "jsonschema[format-nongpl]==', section)
|
|
self.assertNotIn("mkdocs", section)
|
|
self.assertTrue((REPO_ROOT / "website" / "package-lock.json").is_file())
|
|
|
|
|
|
class TheChecklistInstallsWhatCiInstalls(unittest.TestCase):
|
|
"""The pip line named the packages without versions, and nothing said
|
|
that shellcheck, bubblewrap and a Python 3.8 with pyte come from the
|
|
system: their tests skipped locally and failed first in CI."""
|
|
|
|
def test_the_pip_lines_are_the_workflow_lines(self):
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
pinned = [line.split("run: ", 1)[1] for line in workflow.splitlines()
|
|
if re.match(r"\s*run: (python3\.8 -m )?pip install .*==", line)
|
|
and "pyte" in line]
|
|
self.assertEqual(len(pinned), 2)
|
|
for line in pinned:
|
|
with self.subTest(line=line):
|
|
self.assertIn(line, section)
|
|
|
|
def test_it_names_the_system_tools_ci_installs(self):
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
for tool, step in (("shellcheck", "shellcheck --version"),
|
|
("bwrap", "bwrap --ro-bind"),
|
|
("python3.8 -m unittest tests.test_install_tui",
|
|
"python3.8 -m unittest tests.test_install_tui")):
|
|
with self.subTest(tool=tool):
|
|
self.assertIn(step, workflow)
|
|
self.assertIn(tool, section)
|
|
|
|
|
|
class OneFileThroughTheBrowser(unittest.TestCase):
|
|
"""Adding one BIOS asked for a fork, a 5.5 GB clone, five pip packages,
|
|
the data directories and the whole suite, and promised a PR comment that
|
|
a fork's read-only token cannot post. The browser upload into bios/ and
|
|
the CI report already did the work."""
|
|
|
|
def test_the_guide_and_the_issue_chooser_offer_the_upload(self):
|
|
import yaml
|
|
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Add a BIOS file", 1)[1].split("\n## ", 1)[0]
|
|
self.assertIn(generate_readme.UPLOAD_URL, section)
|
|
self.assertNotIn("Fork this repository", section)
|
|
self.assertNotIn("reported in a comment", text)
|
|
chooser = yaml.safe_load(
|
|
(REPO_ROOT / ".github" / "ISSUE_TEMPLATE" / "config.yml").read_text(encoding="utf-8")
|
|
)
|
|
self.assertIn(generate_readme.UPLOAD_URL, [link["url"] for link in chooser["contact_links"]])
|
|
|
|
def test_ci_comments_only_on_branches_of_this_repository(self):
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
self.assertIn("github.event.pull_request.head.repo.full_name == github.repository", workflow)
|
|
self.assertIn('>> "$GITHUB_STEP_SUMMARY"', workflow)
|
|
self.assertIn("summary of the Validation run", generate_readme.generate_contributing())
|
|
|
|
def test_an_upload_at_the_bios_root_is_told_its_folder(self):
|
|
import os
|
|
import tempfile
|
|
|
|
import validate_pr
|
|
|
|
scratch = REPO_ROOT / "tmp" / "tests"
|
|
scratch.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(dir=scratch) as directory:
|
|
(Path(directory) / "bios").mkdir()
|
|
(Path(directory) / "bios" / "boot.rom").write_bytes(b"boot")
|
|
from common import compute_hashes
|
|
|
|
sha1 = compute_hashes(str(Path(directory) / "bios" / "boot.rom"))["sha1"]
|
|
hashes = {"sha1": {sha1}, "folders": {sha1: {"bios/Maker/Console/"}}}
|
|
cwd = os.getcwd()
|
|
os.chdir(directory)
|
|
try:
|
|
result = validate_pr.validate_file("bios/boot.rom", None, hashes)
|
|
finally:
|
|
os.chdir(cwd)
|
|
self.assertTrue(result.passed)
|
|
self.assertTrue(any(
|
|
"bios/ root" in message and "`bios/Maker/Console/`" in message
|
|
for _status, message in result.checks
|
|
))
|
|
|
|
|
|
class TheGuideRebuildsWhenItsPinsMove(unittest.TestCase):
|
|
"""The checklist quotes validate.yml, so a pin bump there must redeploy
|
|
the guide and meet the README/CONTRIBUTING freshness check."""
|
|
|
|
def test_deploy_site_watches_the_validation_workflow(self):
|
|
import yaml
|
|
|
|
deploy = yaml.safe_load(
|
|
(REPO_ROOT / ".github" / "workflows" / "deploy-site.yml").read_text(encoding="utf-8"))
|
|
paths = deploy[True]["push"]["paths"]
|
|
self.assertIn(".github/workflows/validate.yml", paths)
|
|
|
|
|
|
class TheDevPinsAreTheOnesCiInstalls(unittest.TestCase):
|
|
"""Dependabot bumped uv in pyproject.toml alone, while validate.yml and
|
|
CONTRIBUTING kept the older pin: a contributor and CI ran two builds."""
|
|
|
|
PIN = re.compile(r'"([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?==([^"]+)"')
|
|
|
|
def test_a_package_both_pin_has_one_version(self):
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
project = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
|
ci = {name.lower(): version for name, version in self.PIN.findall(workflow)}
|
|
dev = {name.lower(): version for name, version in self.PIN.findall(project)}
|
|
shared = sorted(set(ci) & set(dev))
|
|
self.assertIn("uv", shared)
|
|
for name in shared:
|
|
with self.subTest(package=name):
|
|
self.assertEqual(dev[name], ci[name])
|
|
|
|
|
|
class OneCommandRunsTheChecks(unittest.TestCase):
|
|
"""A contributor copied ten commands by hand before a pull request, and
|
|
skipping one (refresh_data_dirs) failed the pack tests with no hint."""
|
|
|
|
CHECK = re.compile(r"(?:xargs -0 |run: |^\s*)((?:python3?\.?8? -m unittest|python scripts/\w+\.py"
|
|
r"|vermin|npm (?:ci|run build)|mypy-baseline filter|mypy|shellcheck))")
|
|
|
|
def test_every_check_ci_runs_is_a_step(self):
|
|
import pr_checks
|
|
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
ran = {m.group(1) for line in workflow.splitlines()
|
|
if not line.strip().startswith("#") for m in [self.CHECK.search(line)] if m}
|
|
self.assertGreaterEqual(len(ran), 10)
|
|
steps = [c for step in pr_checks.STEPS for c in (step.command, step.then) if c]
|
|
for check in sorted(ran):
|
|
with self.subTest(check=check):
|
|
self.assertTrue(any(command.startswith(check) for command in steps))
|
|
|
|
def test_a_step_ci_lets_fail_does_not_fail_the_run(self):
|
|
import pr_checks
|
|
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
tolerated = set(re.findall(r"continue-on-error: true\n\s*run: (\S+ \S+)", workflow))
|
|
for step in pr_checks.STEPS:
|
|
with self.subTest(step=step.command):
|
|
self.assertEqual(step.may_fail, " ".join(step.command.split()[:2]) in tolerated)
|
|
|
|
def test_steps_keep_ci_order(self):
|
|
import pr_checks
|
|
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
|
|
order = [workflow.index(key) for key in (
|
|
"refresh_data_dirs.py", "run: python scripts/generate_site.py",
|
|
"npm run build --prefix website", "python -m unittest discover",
|
|
"validate_site.py")]
|
|
self.assertEqual(order, sorted(order))
|
|
commands = [step.command for step in pr_checks.STEPS]
|
|
positions = [next(i for i, c in enumerate(commands) if key in c) for key in (
|
|
"refresh_data_dirs.py", "generate_site.py", "npm run build",
|
|
"unittest discover", "validate_site.py")]
|
|
self.assertEqual(positions, sorted(positions))
|
|
|
|
def test_the_guide_gives_the_runner_and_what_it_runs(self):
|
|
import pr_checks
|
|
|
|
text = generate_readme.generate_contributing()
|
|
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
|
|
self.assertIn("python scripts/pr_checks.py\n", section)
|
|
for step in pr_checks.STEPS:
|
|
with self.subTest(step=step.command):
|
|
self.assertIn(step.command, section)
|
|
|
|
def test_a_missing_tool_is_reported_and_a_failure_does_not_stop_the_run(self):
|
|
import contextlib
|
|
import io
|
|
|
|
import pr_checks
|
|
|
|
steps = (
|
|
pr_checks.Step("python -c raise(SystemExit(3))", "fails"),
|
|
pr_checks.Step("no-such-tool-here --x", "absent", needs="no-such-tool-here",
|
|
provider="nothing"),
|
|
pr_checks.Step("python -c pass", "passes"),
|
|
)
|
|
out = io.StringIO()
|
|
with contextlib.redirect_stdout(out):
|
|
code = pr_checks.run(steps)
|
|
self.assertEqual(code, 1)
|
|
self.assertIn("FAILED python -c raise(SystemExit(3)) (exit 3)", out.getvalue())
|
|
self.assertIn("NOT RUN no-such-tool-here --x", out.getvalue())
|
|
self.assertIn("1/3 checks passed", out.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|