mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 14:00:37 +02:00
- "Share working config…" (game menu, `frameport share-recipe`): saves the recipe as known-good and opens a prefilled working-config issue form; labelling it `catalog-accepted` runs catalog-from-issue.yml, which validates the YAML (scripts/catalog_from_issue.py) and opens a PR adding catalog/games/<pkg>.yaml - "Collect logs" / "Report a problem…" (game menu, Settings, failure pop-up, `frameport diag collect|report`): redacted diagnostics zip (app + job logs, library entry, recipe, launch logs + re-triage, APK/ELF/PE listings instead of game files, Frame launcher/settings/Lepton/logcat/XRService logs via agent v21 `collect_diag`) and a prefilled bug-report issue to attach it to; `frameport diag inspect <zip>` re-triages a bundle offline - diag/redact.py scrubs IPs, MACs, host/user names, home dirs, Steam ids, data and dump folders - App log (<data>/logs/app.log) and saved GUI job logs (<data>/logs/jobs/) - catalog.entry_from_library shared by "Save as known-good" and sharing; docs/DIAGNOSTICS.md
270 lines
13 KiB
Python
270 lines
13 KiB
Python
"""PC VR target (Revive + local Windows Steam) against a fake Steam folder; no Windows needed."""
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from frameport.core import winhost
|
|
from frameport.core.events import Reporter
|
|
from frameport.core.models import Recipe
|
|
|
|
AGENT = Path(__file__).resolve().parents[1] / "agent" / "frameport_agent.py"
|
|
|
|
|
|
def vdf_mod():
|
|
spec = importlib.util.spec_from_file_location("agent_for_test", AGENT)
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
def fake_steam(tmp_path):
|
|
root = tmp_path / "Steam"
|
|
(root / "userdata/12345/config").mkdir(parents=True)
|
|
(root / "userdata/999/config").mkdir(parents=True)
|
|
(root / "config").mkdir()
|
|
(root / "steam.exe").write_bytes(b"MZ")
|
|
(root / "config/loginusers.vdf").write_text(
|
|
'"users"\n{\n\t"76561197960278073"\n\t{\n\t\t"AccountName"\t\t"a"\n\t\t"MostRecent"\t\t"1"\n\t}\n'
|
|
'\t"76561197960266727"\n\t{\n\t\t"MostRecent"\t\t"0"\n\t}\n}\n')
|
|
return root
|
|
|
|
|
|
def test_steam_user_most_recent(tmp_path):
|
|
assert winhost.steam_user(fake_steam(tmp_path)) == "12345"
|
|
|
|
|
|
def test_path_translation():
|
|
if not winhost.is_wsl():
|
|
return
|
|
assert winhost.to_windows("/mnt/d/Games/X") == "D:\\Games\\X"
|
|
assert winhost.to_local("C:\\Program Files\\Revive") == Path("/mnt/c/Program Files/Revive")
|
|
|
|
|
|
def test_install_and_shortcut(tmp_path, monkeypatch):
|
|
from frameport.targets import pc_revive
|
|
from frameport.tools import revive
|
|
|
|
root = fake_steam(tmp_path)
|
|
rv = tmp_path / "revive"
|
|
rv.mkdir()
|
|
for n in revive.RUNTIME_FILES:
|
|
(rv / n).write_bytes(b"x")
|
|
monkeypatch.setenv("FRAMEPORT_REVIVE_DIR", str(rv))
|
|
events = []
|
|
monkeypatch.setattr(winhost, "available", lambda: True)
|
|
monkeypatch.setattr(winhost, "is_wsl", lambda: False) # never copy into the real %LOCALAPPDATA%
|
|
monkeypatch.setattr(winhost, "env_path", lambda name: None)
|
|
monkeypatch.setattr(winhost, "steam_root", lambda: root)
|
|
monkeypatch.setattr(winhost, "stop_steam", lambda r: events.append("stop") or True)
|
|
monkeypatch.setattr(winhost, "start_steam", lambda r: events.append("start"))
|
|
monkeypatch.setattr(winhost, "to_windows", lambda p: "W:" + str(p).replace("/", "\\"))
|
|
monkeypatch.setattr("frameport.artwork.fetch.cache.http_get", lambda *a, **k: (_ for _ in ()).throw(OSError()))
|
|
game = tmp_path / "Games" / "Space Game"
|
|
game.mkdir(parents=True)
|
|
(game / "Space Game.exe").write_bytes(b"MZ")
|
|
t = pc_revive.PcReviveTarget()
|
|
r = Recipe("rift.space_game", {"pcvr.revive": {}})
|
|
res = t.install_pcvr("rift.space_game", "Space Game", game, "Space Game.exe", r, Reporter())
|
|
assert res["ok"] and [d["package"] for d in t.installed()] == ["rift.space_game"]
|
|
t.add_to_library(["rift.space_game"], Reporter())
|
|
assert events == ["stop", "start"]
|
|
sc = vdf_mod().vdf_decode((root / "userdata/12345/config/shortcuts.vdf").read_bytes())["shortcuts"]["0"]
|
|
exe_win = "W:" + str(game / "Space Game.exe").replace("/", "\\")
|
|
assert sc["Exe"] == '"' + "W:" + str(rv).replace("/", "\\") + '\\ReviveInjector.exe"'
|
|
assert sc["LaunchOptions"] == f'/openxr "{exe_win}"'
|
|
assert sc["tags"] == {"0": "Rift via Revive", "1": "Oculus Rift"} and sc["appid"] == res["appid"]
|
|
assert sc["StartDir"].endswith('Space Game\\"')
|
|
# OpenVR backend drops /openxr
|
|
r2 = Recipe("rift.space_game", {"pcvr.revive": {}, "pcvr.revive_openvr": {}})
|
|
t.install_pcvr("rift.space_game", "Space Game", game, "Space Game.exe", r2, Reporter())
|
|
assert pc_revive.shortcut_fields(t._dep("rift.space_game"))[2] == f'"{exe_win}"'
|
|
assert t.uninstall("rift.space_game")["removed"] and t.installed() == []
|
|
|
|
|
|
def test_openxr_native_runs_exe_directly():
|
|
from frameport.targets.pc_revive import shortcut_fields
|
|
|
|
exe, start, opts = shortcut_fields({"exe_win": "D:\\G\\Game.exe", "revive_win": None})
|
|
assert (exe, start, opts) == ('"D:\\G\\Game.exe"', '"D:\\G\\"', "")
|
|
|
|
|
|
def test_no_crash_reporter_patch(tmp_path):
|
|
from types import SimpleNamespace
|
|
|
|
from frameport.core.models import Recipe
|
|
from frameport.patches import pcvr
|
|
from frameport.patches.base import get
|
|
|
|
p = get("pcvr.no_crash_reporter")
|
|
ue = SimpleNamespace(engine="Unreal", extra={"kind": "rift"})
|
|
unity = SimpleNamespace(engine="Unity", extra={"kind": "rift"})
|
|
assert p.detect(ue).recommended and p.applies(ue)
|
|
assert p.detect(unity) is None and not p.applies(unity)
|
|
assert pcvr.game_args(Recipe(package="rift.x", patches=["pcvr.no_crash_reporter"])) == ["-nocrashreports"]
|
|
assert pcvr.game_args(Recipe(package="rift.x", patches=[])) == []
|
|
|
|
|
|
def test_agent_crash_reporter_toggle(tmp_path):
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
spec = importlib.util.spec_from_file_location("fpa", Path(__file__).parents[1] / "agent" / "frameport_agent.py")
|
|
agent = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(agent)
|
|
exe = tmp_path / "game" / "Engine" / "Binaries" / "Win64" / "CrashReportClient.exe"
|
|
exe.parent.mkdir(parents=True)
|
|
exe.write_bytes(b"MZ")
|
|
agent.set_crash_reporter(str(tmp_path), enabled=False)
|
|
assert not exe.exists() and exe.with_name("CrashReportClient.exe.disabled").exists()
|
|
# still counts as uploaded (not re-sent on the next install)
|
|
assert "Engine/Binaries/Win64/CrashReportClient.exe" in agent.tree_manifest(str(tmp_path / "game"))
|
|
agent.set_crash_reporter(str(tmp_path), enabled=True)
|
|
assert exe.exists()
|
|
|
|
|
|
def test_library_migration_adds_no_crash_reporter(tmp_path, monkeypatch):
|
|
import json
|
|
|
|
from frameport.core import library
|
|
|
|
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
|
|
(tmp_path / "library.json").write_text(json.dumps({"games": {
|
|
"rift.ue": {"analysis": {"engine": "Unreal", "extra": {"kind": "rift"}}, "recipe": {"patches": {}}},
|
|
"rift.unity": {"analysis": {"engine": "Unity", "extra": {"kind": "rift"}}, "recipe": {"patches": {}}},
|
|
}, "settings": {}}))
|
|
games = library.load()["games"]
|
|
# the run-correct migration re-derives from engine defaults: Oculus games get Revive + as-is
|
|
assert set(games["rift.ue"]["recipe"]["patches"]) == {"pcvr.revive", "pcvr.revive_openvr", "pcvr.libovr_redirect",
|
|
"pcvr.no_crash_reporter", "pcvr.oculus_unreal", "pcvr.xr_timefix"}
|
|
assert set(games["rift.unity"]["recipe"]["patches"]) == {"pcvr.revive", "pcvr.revive_openvr",
|
|
"pcvr.libovr_redirect", "pcvr.xr_timefix"}
|
|
assert games["rift.ue"]["recipe"]["as_is"] and games["rift.unity"]["recipe"]["as_is"]
|
|
# runs once: a user who turns it off keeps it off
|
|
data = library.load()
|
|
data["games"]["rift.ue"]["recipe"]["patches"].pop("pcvr.no_crash_reporter")
|
|
library.save(data)
|
|
assert "pcvr.no_crash_reporter" not in library.load()["games"]["rift.ue"]["recipe"]["patches"]
|
|
|
|
|
|
def test_oculus_unreal_patch():
|
|
from types import SimpleNamespace
|
|
|
|
from frameport.patches.base import get
|
|
|
|
p = get("pcvr.oculus_unreal")
|
|
ue = SimpleNamespace(engine="Unreal", extra={"kind": "rift"})
|
|
ue_xr = SimpleNamespace(engine="Unreal", extra={"kind": "rift", "frame_native": True})
|
|
unity = SimpleNamespace(engine="Unity", extra={"kind": "rift"})
|
|
quest_ue = SimpleNamespace(engine="Unreal", extra={})
|
|
assert p.category == "pcvr" and p.detect(ue).recommended and p.applies(ue)
|
|
assert not p.detect(ue_xr).recommended and p.applies(ue_xr)
|
|
assert p.detect(unity) is None and not p.applies(unity)
|
|
assert p.detect(quest_ue) is None and not p.applies(quest_ue)
|
|
|
|
|
|
def test_migration_rederives_rift_recipes(tmp_path, monkeypatch):
|
|
import json
|
|
|
|
"""The run-correct migration re-derives every Rift recipe from engine defaults: Oculus games get Revive for VR."""
|
|
from frameport.core import library
|
|
|
|
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
|
|
(tmp_path / "library.json").write_text(json.dumps({"games": {
|
|
"rift.ue": {"analysis": {"engine": "Unreal", "abis": ["x86_64"], "extra": {"kind": "rift", "needs_revive": True}},
|
|
"recipe": {"as_is": True, "patches": {}}}, # Revive wrongly dropped -> restored
|
|
"com.q.ue": {"analysis": {"engine": "Unreal", "extra": {}}, "recipe": {"patches": {}}},
|
|
}, "settings": {}}))
|
|
games = library.load()["games"]
|
|
r = games["rift.ue"]["recipe"]
|
|
assert r["as_is"] and "pcvr.revive" in r["patches"] and "pcvr.oculus_unreal" in r["patches"]
|
|
assert games["com.q.ue"]["recipe"]["patches"] == {} # Quest games untouched
|
|
|
|
def test_oculus_hmd_helper_artifact():
|
|
"""The prebuilt helper is a freestanding Windows x64 console exe that only imports kernel32."""
|
|
import struct
|
|
|
|
from frameport.core.paths import artifacts_dir
|
|
|
|
data = (artifacts_dir() / "win-x64" / "fp_oculushmd.exe").read_bytes()
|
|
assert data[:2] == b"MZ"
|
|
pe = struct.unpack_from("<I", data, 0x3C)[0]
|
|
assert data[pe:pe + 4] == b"PE\0\0"
|
|
assert struct.unpack_from("<H", data, pe + 4)[0] == 0x8664 # AMD64
|
|
assert struct.unpack_from("<H", data, pe + 24)[0] == 0x20B # PE32+
|
|
assert struct.unpack_from("<H", data, pe + 24 + 68)[0] == 3 # console subsystem (stderr -> launch.log)
|
|
assert b"kernel32.dll" in data and b"CreateEventW" in data
|
|
assert "OculusHMDConnected".encode("utf-16-le") in data
|
|
assert not any(dll in data.lower() for dll in (b"msvcrt", b"vcruntime", b"ucrtbase"))
|
|
|
|
|
|
def test_oculus_hmd_triage():
|
|
from frameport.validate.triage import triage
|
|
|
|
ok = triage("FramePort: launching rift.x with proton_11-arm64\n"
|
|
"FramePort oculushmd: OculusHMDConnected event ready\n", package="rift.x")
|
|
assert "Oculus headset event provided" in ok.milestones and not ok.findings
|
|
bad = triage("FramePort oculushmd: could not create the OculusHMDConnected event (error 5)\n", package="rift.x")
|
|
assert [f.id for f in bad.findings] == ["oculus-hmd-event"]
|
|
assert "pcvr.oculus_unreal" in bad.findings[0].suggest
|
|
|
|
|
|
def test_migration_respects_catalog_openxr_native(tmp_path, monkeypatch):
|
|
import json
|
|
|
|
"""A catalog SteamVR/OpenXR-native recipe (Revive removed) keeps Revive off after the re-derive."""
|
|
from frameport.core import library
|
|
|
|
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
|
|
(tmp_path / "library.json").write_text(json.dumps({"games": {
|
|
# openxr_native -> engine.suggest doesn't add Revive
|
|
"rift.xr": {"analysis": {"engine": "Unity", "abis": ["x86_64"],
|
|
"extra": {"kind": "rift", "openxr_native": True}},
|
|
"recipe": {"as_is": True, "patches": {"pcvr.revive": {}}}},
|
|
}, "settings": {}}))
|
|
r = library.load()["games"]["rift.xr"]["recipe"]
|
|
assert "pcvr.revive" not in r["patches"]
|
|
|
|
|
|
def test_pc_defaults_to_openvr_backend():
|
|
from frameport.patches import base
|
|
from frameport.targets.pc_revive import shortcut_fields
|
|
|
|
p = base.get("pcvr.revive_openvr")
|
|
assert p.detect(SimpleNamespace(extra={"kind": "rift"})).recommended # on by default (PC)
|
|
assert not p.detect(SimpleNamespace(extra={"kind": "rift", "frame_native": True})).recommended
|
|
# OpenVR backend -> no /openxr flag in the launch command
|
|
dep_ovr = {"exe_win": "C:\\g\\Game.exe", "revive_win": "C:\\Revive", "backend": "openvr", "game_args": []}
|
|
dep_xr = {**dep_ovr, "backend": "openxr"}
|
|
assert "/openxr" not in shortcut_fields(dep_ovr)[2] and "Game.exe" in shortcut_fields(dep_ovr)[2]
|
|
assert "/openxr" in shortcut_fields(dep_xr)[2]
|
|
|
|
|
|
def test_start_steamvr(monkeypatch, tmp_path):
|
|
from frameport.core import winhost
|
|
|
|
running = {"vr": False}
|
|
monkeypatch.setattr(winhost, "process_running", lambda img: running["vr"] if "vr" in img.lower() else False)
|
|
monkeypatch.setattr(winhost, "app_installed", lambda root, appid: True)
|
|
started = []
|
|
monkeypatch.setattr(winhost, "start_detached", lambda exe, args=(), cwd=None: (started.append(list(args)),
|
|
running.__setitem__("vr", True))[0])
|
|
assert winhost.start_steamvr(tmp_path, wait=5) is True and started == [["-applaunch", "250820"]]
|
|
# already running -> no-op
|
|
started.clear()
|
|
assert winhost.start_steamvr(tmp_path) is True and started == []
|
|
# not installed -> False, nothing started
|
|
running["vr"] = False
|
|
monkeypatch.setattr(winhost, "app_installed", lambda root, appid: False)
|
|
assert winhost.start_steamvr(tmp_path, wait=2) is False and started == []
|
|
|
|
|
|
def test_libovr_redirect_patch():
|
|
from frameport.patches.base import get
|
|
|
|
p = get("pcvr.libovr_redirect")
|
|
assert p.requires == ("pcvr.revive",)
|
|
assert p.detect(SimpleNamespace(extra={"kind": "rift"})).recommended
|
|
assert not p.detect(SimpleNamespace(extra={"kind": "rift", "frame_native": True})).recommended
|
|
assert p.detect(SimpleNamespace(extra={})) is None
|