mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
fix: load every script under the 3.9 floor
A module-level alias written with a PEP 604 union compiled under 3.9 and raised TypeError when check_release_assets.py, and check_release.py through it, were loaded. The floor test now imports every module under scripts/ with a real 3.9 interpreter, which CI gives pyyaml and jsonschema.
This commit is contained in:
1 parent
0242dc77ee
commit
8721dafd2a
3 files changed
+56
-22
No files matched your search
@@ -195,8 +195,10 @@ jobs:
|
||||
|
||||
# install.py stays on Python 3.8: the suite runs it end to end under a
|
||||
# real 3.8 interpreter, which import-only and grammar checks miss. 3.9
|
||||
# is the floor pipeline.py states for the build scripts, compiled under
|
||||
# a real 3.9. The last version listed is the default python.
|
||||
# is the floor pipeline.py states for the build scripts: a real 3.9
|
||||
# imports every module under scripts/, so it needs their dependencies.
|
||||
# jsonschema 4.26 requires 3.10, 4.25.1 is the last release for 3.9.
|
||||
# The last version listed is the default python.
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: |
|
||||
@@ -204,6 +206,9 @@ jobs:
|
||||
3.9
|
||||
3.12
|
||||
|
||||
- name: Install the build scripts' dependencies for Python 3.9
|
||||
run: python3.9 -m pip install pyyaml "jsonschema[format-nongpl]==4.25.1"
|
||||
|
||||
# markdown: the site validation test renders a wiki line to prove the
|
||||
# heading guard's premise, and skipped on every run without it. The docs
|
||||
# group builds the site as deploy-site.yml does, with the same bounds.
|
||||
|
||||
@@ -22,6 +22,7 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import NamedTuple
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import composition_tier, load_database
|
||||
@@ -32,7 +33,14 @@ from largefiles import (
|
||||
registered_paths,
|
||||
)
|
||||
|
||||
Finding = tuple[str, str, int, int | None]
|
||||
|
||||
class AssetFinding(NamedTuple):
|
||||
"""A difference between the release and the collection."""
|
||||
|
||||
kind: str
|
||||
path: str
|
||||
size: int
|
||||
published: int | None
|
||||
|
||||
|
||||
def expected_assets(db: dict, gitignore_text: str) -> dict[str, int]:
|
||||
@@ -61,7 +69,7 @@ def compare(
|
||||
registered: Iterable[str] | None = None,
|
||||
expected_digests: dict[str, str] | None = None,
|
||||
asset_digests: dict[str, str] | None = None,
|
||||
) -> list[Finding]:
|
||||
) -> list[AssetFinding]:
|
||||
"""Files the release lacks or serves at another size, sorted by kind.
|
||||
|
||||
At the same size, the sha256 the release lists for an asset is held
|
||||
@@ -74,9 +82,9 @@ def compare(
|
||||
matches what render_notes() would write is a finding of its own: the
|
||||
page is what a reader checks a download against.
|
||||
"""
|
||||
findings: list[Finding] = []
|
||||
findings: list[AssetFinding] = []
|
||||
if not notes_current:
|
||||
findings.append(("notes", "release description", 0, None))
|
||||
findings.append(AssetFinding("notes", "release description", 0, None))
|
||||
names = asset_names([*(registered or ()), *expected])
|
||||
for path, size in sorted(expected.items()):
|
||||
served = next(
|
||||
@@ -84,14 +92,14 @@ def compare(
|
||||
)
|
||||
published = assets[served] if served else None
|
||||
if published is None:
|
||||
findings.append(("missing", path, size, None))
|
||||
findings.append(AssetFinding("missing", path, size, None))
|
||||
elif published != size:
|
||||
findings.append(("size", path, size, published))
|
||||
findings.append(AssetFinding("size", path, size, published))
|
||||
else:
|
||||
ours = (expected_digests or {}).get(path)
|
||||
theirs = (asset_digests or {}).get(served)
|
||||
if ours and theirs and ours != theirs:
|
||||
findings.append(("content", path, size, published))
|
||||
findings.append(AssetFinding("content", path, size, published))
|
||||
return sorted(findings)
|
||||
|
||||
|
||||
|
||||
@@ -110,11 +110,12 @@ class ReadmeRegressions(unittest.TestCase):
|
||||
self.assertEqual(proc.returncode, 1)
|
||||
self.assertIn("needs Python 3.9", proc.stderr)
|
||||
|
||||
def test_the_floor_interpreter_compiles_every_script(self):
|
||||
"""The floor said 3.12 for a reason the code no longer carried: every
|
||||
script compiled under 3.9, and only the guard refused it. Checked
|
||||
with a real interpreter of the floor version, as the installer's 3.8
|
||||
floor is."""
|
||||
def test_the_floor_interpreter_imports_every_script(self):
|
||||
"""Every module under scripts/ is loaded by a real interpreter of the
|
||||
floor version, as the installer's 3.8 floor is proven by running it.
|
||||
Compiling only checks the grammar: a module-level alias such as
|
||||
`tuple[int, int | None]` compiles under 3.9 and raises TypeError
|
||||
when the module loads."""
|
||||
import shutil # noqa: PLC0415
|
||||
|
||||
floor = re.search(
|
||||
@@ -128,21 +129,41 @@ class ReadmeRegressions(unittest.TestCase):
|
||||
self.fail(f"CI must provide {name}")
|
||||
self.skipTest(f"no {name} on PATH")
|
||||
check = (
|
||||
"import pathlib, sys\n"
|
||||
"bad = []\n"
|
||||
"import importlib, json, pathlib, sys\n"
|
||||
"sys.path.insert(0, '.')\n"
|
||||
"failed = {}\n"
|
||||
"for p in sorted(pathlib.Path('scripts').rglob('*.py')):\n"
|
||||
" if '__pycache__' in p.parts:\n"
|
||||
" continue\n"
|
||||
" parts = p.with_suffix('').parts\n"
|
||||
" if parts[-1] == '__init__':\n"
|
||||
" parts = parts[:-1]\n"
|
||||
" try:\n"
|
||||
" compile(p.read_text(encoding='utf-8'), str(p), 'exec')\n"
|
||||
" except SyntaxError as e:\n"
|
||||
" bad.append(f'{p}:{e.lineno}')\n"
|
||||
"print(bad)\n"
|
||||
"sys.exit(1 if bad else 0)\n"
|
||||
" importlib.import_module('.'.join(parts))\n"
|
||||
" except BaseException as e:\n"
|
||||
" failed[str(p)] = [type(e).__name__, getattr(e, 'name', None), str(e)]\n"
|
||||
"print(json.dumps(failed))\n"
|
||||
)
|
||||
proc = subprocess.run(
|
||||
[interpreter, "-I", "-c", check], cwd=ROOT, capture_output=True,
|
||||
text=True, timeout=300, check=False,
|
||||
text=True, timeout=600, check=False,
|
||||
env={**os.environ, "RETROBIOS_OFFLINE": "1"},
|
||||
)
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
failed = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
# pyyaml for every build script, jsonschema for validate_schemas:
|
||||
# without them the floor interpreter cannot load the modules, which
|
||||
# proves nothing either way.
|
||||
absent = sorted({
|
||||
err[1] for err in failed.values()
|
||||
if err[0] == "ModuleNotFoundError" and err[1] in ("yaml", "jsonschema")
|
||||
})
|
||||
if absent:
|
||||
message = f"{name} lacks {', '.join(absent)}"
|
||||
if os.environ.get("CI"):
|
||||
self.fail(message)
|
||||
self.skipTest(message)
|
||||
self.assertEqual(failed, {})
|
||||
|
||||
|
||||
class ReleaseSigningRegressions(unittest.TestCase):
|
||||
|
||||
Reference in new issue
Block a user