217 lines
7.3 KiB
Python
217 lines
7.3 KiB
Python
#!/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())
|