#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Host-only policy tests for the Phase-1.0B smoke candidate.""" from __future__ import annotations import argparse import importlib.util from pathlib import Path import sys from types import ModuleType from typing import Callable def load_module(name: str, path: Path) -> ModuleType: spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: raise RuntimeError(f"could not load {path}") module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module def require(condition: bool, message: str) -> None: if not condition: raise RuntimeError(message) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) args = parser.parse_args() root = args.root.resolve() validator = load_module( "phase10b_validator", root / "tools/validate_retroarch_phase10b.py" ) cases: list[tuple[str, Callable[[], None]]] = [] def case(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]: def register(function: Callable[[], None]) -> Callable[[], None]: cases.append((name, function)) return function return register def profile(**changes: object) -> dict[str, object]: value: dict[str, object] = { "disabled_features": sorted(validator.DISABLED_FEATURES), "persistent_writes_allowed": False, "content_required": False, "config_required": False, "networking": False, "autoload": False, "automatic_retry": False, } value.update(changes) return value def route(**changes: object) -> dict[str, object]: value: dict[str, object] = { "write": False, "create": False, "append": False, "truncate": False, "rename": False, "unlink": False, "mkdir": False, "retry": False, } value.update(changes) return value @case("01 profile is closed") def _() -> None: require(validator.profile_is_closed(profile()), "closed profile rejected") @case("02 networking is rejected") def _() -> None: require(not validator.profile_is_closed(profile(networking=True)), "network accepted") @case("03 content is not required") def _() -> None: require(not validator.profile_is_closed(profile(content_required=True)), "content accepted") @case("04 config is not required") def _() -> None: require(not validator.profile_is_closed(profile(config_required=True)), "config accepted") @case("05 automatic retry is rejected") def _() -> None: require(not validator.profile_is_closed(profile(automatic_retry=True)), "retry accepted") @case("06 autoload is rejected") def _() -> None: require(not validator.profile_is_closed(profile(autoload=True)), "autoload accepted") @case("07 write is rejected") def _() -> None: require(not validator.route_is_read_only(route(write=True)), "write accepted") @case("08 create is rejected") def _() -> None: require(not validator.route_is_read_only(route(create=True)), "create accepted") @case("09 append is rejected") def _() -> None: require(not validator.route_is_read_only(route(append=True)), "append accepted") @case("10 truncate is rejected") def _() -> None: require(not validator.route_is_read_only(route(truncate=True)), "truncate accepted") @case("11 rename is rejected") def _() -> None: require(not validator.route_is_read_only(route(rename=True)), "rename accepted") @case("12 unlink is rejected") def _() -> None: require(not validator.route_is_read_only(route(unlink=True)), "unlink accepted") @case("13 mkdir is rejected") def _() -> None: require(not validator.route_is_read_only(route(mkdir=True)), "mkdir accepted") @case("14 route retry is rejected") def _() -> None: require(not validator.route_is_read_only(route(retry=True)), "route retry accepted") @case("15 RX/R/RW headers pass") def _() -> None: headers = [ {"type": "LOAD", "flags": "R E"}, {"type": "LOAD", "flags": "R"}, {"type": "LOAD", "flags": "RW"}, ] require(validator.program_headers_are_wx_closed(headers), "W^X headers rejected") @case("16 RWE header fails") def _() -> None: headers = [ {"type": "LOAD", "flags": "RWE"}, {"type": "LOAD", "flags": "RW"}, {"type": "LOAD", "flags": "RW"}, ] require(not validator.program_headers_are_wx_closed(headers), "RWE accepted") @case("17 SceNet import fails") def _() -> None: require(not validator.imports_are_closed(["sceNetSocket"]), "SceNet accepted") @case("18 GNM import fails") def _() -> None: require(not validator.imports_are_closed(["sceGnmSubmitCommandBuffers"]), "GNM accepted") @case("19 module loading fails") def _() -> None: require(not validator.imports_are_closed(["sceKernelLoadStartModule"]), "module loader accepted") @case("20 rumble fails") def _() -> None: require(not validator.imports_are_closed(["scePadSetVibration"]), "rumble accepted") @case("21 lightbar fails") def _() -> None: require(not validator.imports_are_closed(["scePadSetLightBar"]), "lightbar accepted") @case("22 socket import fails") def _() -> None: require(not validator.imports_are_closed(["socket"]), "socket accepted") @case("23 expected imports pass") def _() -> None: require( validator.imports_are_closed( ["sceVideoOutOpen", "scePadOpen", "sceAudioOutOpen", "_Exit"] ), "expected imports rejected", ) @case("24 runtime contract pins both deadlines") def _() -> None: text = (root / "docs/retroarch/phase-1.0b-smoke-candidate-design.md").read_text() require("60,000 ms" in text and "3,600" in text, "deadlines absent") @case("25 exit risk remains explicit") def _() -> None: text = (root / "docs/retroarch/phase-1.0b-runtime-and-exit-contract.md").read_text() require("unproven" in text.lower() and "_Exit" in text, "exit risk hidden") @case("26 host evidence is not hardware proof") def _() -> None: text = (root / "docs/retroarch/phase-1.0b-smoke-candidate-design.md").read_text() require( "no host test or static audit" in text.lower() and "is hardware evidence" in text.lower(), "hardware disclaimer absent", ) failures: list[str] = [] for name, function in cases: try: function() except Exception as error: # noqa: BLE001 - isolated reporting failures.append(f"{name}: {error}") if len(cases) != 26: failures.append(f"expected 26 cases, found {len(cases)}") if failures: print("\n".join(failures), file=sys.stderr) return 1 print("26 Phase 1.0B guardrails passed") return 0 if __name__ == "__main__": raise SystemExit(main())