mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-19 15:01:36 +02:00
275 lines
14 KiB
Python
275 lines
14 KiB
Python
"""Quest button preferences: inheritance, deployment payloads and generated mapping assets."""
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import flet as ft
|
|
import pytest
|
|
from test_game_update import game_and_dep, installed_agent
|
|
from test_patches import _analysis
|
|
|
|
from frameport import pipeline
|
|
from frameport.core import controllers, library, paths
|
|
from frameport.core.events import Cancelled, Reporter
|
|
from frameport.core.models import Recipe
|
|
from frameport.game_update import configuration, configuration_fingerprint, plan_update
|
|
from frameport.install.installer import configuration_args, install_context
|
|
from frameport.patches import base
|
|
from frameport.patches.frame.adapter import settings_text
|
|
from frameport.patches.settings import SETTINGS, UI, XRSHIM_SETTINGS, adapter_settings
|
|
from frameport.ui.views import adapter_dialog as ad
|
|
from frameport.ui.views.settings import SettingsView
|
|
|
|
|
|
def store(patches=None):
|
|
recipe = Recipe("test.quest", patches=patches or {"frame.adapter": {}})
|
|
library.upsert_game(recipe.package, title="Quest", recipe=library.recipe_to_dict(recipe))
|
|
return library.game(recipe.package)
|
|
|
|
|
|
def walk(control):
|
|
yield control
|
|
for child in getattr(control, "controls", []) or []:
|
|
yield from walk(child)
|
|
for attr in ("content", "title"):
|
|
child = getattr(control, attr, None)
|
|
if child is not None and not isinstance(child, str):
|
|
yield from walk(child)
|
|
for child in getattr(control, "actions", []) or []:
|
|
yield from walk(child)
|
|
|
|
|
|
def test_setting_metadata_and_no_suggestion():
|
|
spec = next(s for s in SETTINGS if s[0] == "quest_buttons")
|
|
assert spec[1:3] == ("int", 0)
|
|
assert "docs/CONTROLLERS.md" in spec[4]
|
|
assert UI["quest_buttons"]["control"] == ("choice", list(controllers.CHOICES))
|
|
assert UI["quest_buttons"]["group"] == "controllers" and UI["quest_buttons"]["level"] == "common"
|
|
patch = base.get(controllers.QUEST_BUTTONS)
|
|
assert patch.detect(_analysis()) is None
|
|
assert patch.a32 and "quest_buttons" not in XRSHIM_SETTINGS
|
|
assert patch.applies(_analysis(abis=["armeabi-v7a"]))
|
|
|
|
|
|
@pytest.mark.parametrize("global_mode", [0, 1, 2])
|
|
@pytest.mark.parametrize("override", [None, 0, 1, 2])
|
|
def test_effective_value_reaches_every_runtime_payload(global_mode, override):
|
|
library.set_setting("input.quest_buttons", global_mode)
|
|
patches = {controllers.QUEST_BUTTONS: {"value": override}} if override is not None else {}
|
|
game = store(patches or {"frame.adapter": {}})
|
|
recipe = library.recipe_from_dict(game["recipe"])
|
|
expected = global_mode if override is None else override
|
|
assert controllers.effective_adapter_settings(recipe.patches)["quest_buttons"] == expected
|
|
assert install_context(recipe).adapter_settings["quest_buttons"] == expected
|
|
args = configuration_args(game, recipe)
|
|
assert args["settings"]["quest_buttons"] == expected
|
|
assert f"quest_buttons={expected}\n".encode() in settings_text(recipe.patches)
|
|
assert ad.recipe_values(game)["quest_buttons"] == expected
|
|
# Fingerprinting is a pure operation on the recipe, not the active library preference.
|
|
assert args["configuration"]["settings"]["quest_buttons"] == (override or 0)
|
|
|
|
|
|
def test_pure_defaults_and_global_changes_do_not_mark_games_outdated(monkeypatch):
|
|
def forbidden(*args):
|
|
raise AssertionError("pure settings helper performed I/O")
|
|
with monkeypatch.context() as m:
|
|
m.setattr(library, "setting", forbidden)
|
|
assert adapter_settings({}, defaults={"quest_buttons": 2})["quest_buttons"] == 2
|
|
assert adapter_settings({controllers.QUEST_BUTTONS: {"value": 0}},
|
|
defaults={"quest_buttons": 2})["quest_buttons"] == 0
|
|
game, dep = game_and_dep()
|
|
before = base.recipe_fingerprint(game["recipe"])
|
|
config = configuration_fingerprint(game["recipe"])
|
|
for mode in (1, 2, 0):
|
|
library.set_setting("input.quest_buttons", mode)
|
|
assert base.recipe_fingerprint(game["recipe"]) == before
|
|
assert configuration_fingerprint(game["recipe"]) == config
|
|
assert not plan_update(game, dep).changed
|
|
|
|
|
|
def test_saving_game_keeps_inheritance_and_explicit_zero():
|
|
library.set_setting("input.quest_buttons", 2)
|
|
game = store()
|
|
ad.save_to_recipe(game["package"], ad.recipe_values(game))
|
|
assert library.game(game["package"])["recipe"] == game["recipe"]
|
|
values = ad.recipe_values(game)
|
|
values["quest_buttons"] = 0
|
|
ad.save_to_recipe(game["package"], values, inherit_quest_buttons=False)
|
|
assert library.game(game["package"])["recipe"]["patches"][controllers.QUEST_BUTTONS] == {"value": 0}
|
|
ad.save_to_recipe(game["package"], values, inherit_quest_buttons=True)
|
|
assert controllers.QUEST_BUTTONS not in library.game(game["package"])["recipe"]["patches"]
|
|
assert ad.recipe_values(library.game(game["package"]))["quest_buttons"] == 2
|
|
|
|
|
|
def test_agent_live_default_write_does_not_mark_inherited_game_outdated(monkeypatch, tmp_path):
|
|
game, _ = game_and_dep()
|
|
package = game["package"]
|
|
agent, _, record = installed_agent(monkeypatch, tmp_path)
|
|
record.update(configuration=configuration(game["recipe"], package),
|
|
configuration_fp=configuration_fingerprint(game["recipe"], package))
|
|
agent.write_deployment(package, record)
|
|
for mode in (1, 2, 0):
|
|
agent.cmd_set_settings({"package": package, "settings": {"quest_buttons": mode}})
|
|
deployed = agent.deployment(package)
|
|
assert not plan_update(game, deployed).changed
|
|
agent.cmd_set_settings({"package": package, "settings": {"quest_buttons": 1}})
|
|
deployed = agent.deployment(package)
|
|
# Explicit overrides still need to match, and unrelated runtime changes are never ignored.
|
|
game["recipe"]["patches"][controllers.QUEST_BUTTONS] = {"value": 0}
|
|
assert plan_update(game, deployed).configure
|
|
game["recipe"]["patches"].pop(controllers.QUEST_BUTTONS)
|
|
game["recipe"]["patches"]["adapter.scale"] = {"value": 1.8}
|
|
assert plan_update(game, deployed).configure
|
|
|
|
|
|
def test_dialog_global_option_and_diagram_follow_choice():
|
|
library.set_setting("input.quest_buttons", 1)
|
|
game = store()
|
|
dialogs = []
|
|
app = SimpleNamespace(page=SimpleNamespace(show_dialog=dialogs.append, pop_dialog=lambda: None),
|
|
target=None, toast=lambda *a, **k: None, refresh_view=lambda: None,
|
|
_title=lambda p: "Quest")
|
|
ad._open(app, game["package"], game, ad.recipe_values(game))
|
|
controls = list(walk(dialogs[-1]))
|
|
choice = next(c for c in controls if isinstance(c, ft.Dropdown) and any(o.key == "global" for o in c.options))
|
|
image = next(c for c in controls if isinstance(c, ft.Image) and "quest-buttons" in c.src)
|
|
assert choice.value == "global" and "Mirror" in choice.options[0].text
|
|
assert image.src.endswith("quest-buttons-1.svg")
|
|
choice.on_select(SimpleNamespace(control=SimpleNamespace(value="0")))
|
|
assert image.src.endswith("quest-buttons-0.svg")
|
|
dialogs[-1].actions[-1].on_click(None)
|
|
assert library.game(game["package"])["recipe"]["patches"][controllers.QUEST_BUTTONS] == {"value": 0}
|
|
|
|
|
|
def test_dialog_reset_restores_global_value_without_pinning_it():
|
|
library.set_setting("input.quest_buttons", 2)
|
|
game = store({"frame.adapter": {}, controllers.QUEST_BUTTONS: {"value": 1}})
|
|
dialogs = []
|
|
app = SimpleNamespace(page=SimpleNamespace(show_dialog=dialogs.append, pop_dialog=lambda: None),
|
|
target=None, toast=lambda *a, **k: None, refresh_view=lambda: None,
|
|
_title=lambda p: "Quest")
|
|
ad._open(app, game["package"], game, ad.recipe_values(game))
|
|
dialogs[-1].actions[0].on_click(None)
|
|
assert next(c for c in walk(dialogs[-1]) if isinstance(c, ft.Image) and "quest-buttons" in c.src
|
|
).src.endswith("quest-buttons-2.svg")
|
|
dialogs[-1].actions[-1].on_click(None)
|
|
assert controllers.QUEST_BUTTONS not in library.game(game["package"])["recipe"]["patches"]
|
|
|
|
|
|
class Target:
|
|
def __init__(self, installed):
|
|
self.games, self.calls = installed, []
|
|
|
|
def installed(self):
|
|
return self.games
|
|
|
|
def set_settings(self, package, values):
|
|
self.calls.append((package, values))
|
|
|
|
|
|
def test_apply_default_skips_overrides_and_non_quest_games():
|
|
library.set_setting("input.quest_buttons", 2)
|
|
store({controllers.QUEST_BUTTONS: {"value": 0}})
|
|
library.upsert_game("flat", analysis={"extra": {"vr_kind": "none"}})
|
|
target = Target([{"package": "test.quest"}, {"package": "inherited"}, {"package": "flat"},
|
|
{"package": "rift", "kind": "pcvr"}, {"package": "linux", "kind": "linux"},
|
|
{"package": "remote-override", "recipe": {"patches": [controllers.QUEST_BUTTONS]}}])
|
|
assert controllers.apply_default(target, Reporter()) == {"applied": [], "needs_update": ["inherited"]}
|
|
assert target.calls == [("inherited", {"quest_buttons": 2})]
|
|
reporter = Reporter()
|
|
reporter.cancelled.set()
|
|
with pytest.raises(Cancelled):
|
|
controllers.apply_default(target, reporter)
|
|
assert len(target.calls) == 1
|
|
|
|
|
|
@pytest.fixture
|
|
def adapter_manifest(monkeypatch, tmp_path):
|
|
manifest = tmp_path / "artifacts" / "SHA256SUMS"
|
|
manifest.parent.mkdir()
|
|
manifest.write_text("\n".join(f"{'a' * 64} ./{abi}/libopenxr_loader_generic.so"
|
|
for abi in ("arm64-v8a", "armeabi-v7a")) + "\n")
|
|
monkeypatch.setattr(paths, "artifacts_dir", lambda: manifest.parent)
|
|
return manifest
|
|
|
|
|
|
@pytest.mark.parametrize("abi", ["arm64-v8a", "armeabi-v7a"])
|
|
@pytest.mark.parametrize(("recorded", "installed_sha", "ready"), [
|
|
("a" * 64, "apk", True),
|
|
("a" * 64, "alternate", True),
|
|
("b" * 64, "apk", False),
|
|
(None, "apk", False),
|
|
("a" * 64, "older-apk", False),
|
|
("a" * 64, None, False),
|
|
])
|
|
def test_apply_reports_current_old_and_unknown_adapters(adapter_manifest, abi, recorded, installed_sha, ready):
|
|
game = store()
|
|
native = {f"{abi}/libopenxr_loader_generic.so": recorded, f"{abi}/unrelated.so": "old"}
|
|
library.set_fields(game["package"], build={"sha256": "apk", "alt_sha256": "alternate",
|
|
"build_inputs": {"native": native}})
|
|
target = Target([{"package": game["package"], "sha256": installed_sha}])
|
|
result = controllers.apply_default(target, Reporter())
|
|
assert result == {"applied": [game["package"]] if ready else [],
|
|
"needs_update": [] if ready else [game["package"]]}
|
|
assert target.calls == [(game["package"], {"quest_buttons": 0})]
|
|
|
|
|
|
def test_apply_does_not_trust_unknown_adapter_or_manifest(adapter_manifest):
|
|
game = store()
|
|
library.set_fields(game["package"], build={"sha256": "apk", "build_inputs": {"native": {}}})
|
|
target = Target([{"package": game["package"], "sha256": "apk"}])
|
|
assert controllers.apply_default(target, Reporter())["needs_update"] == [game["package"]]
|
|
library.set_fields(game["package"], build={"sha256": "apk", "build_inputs": {"native": {
|
|
"arm64-v8a/libopenxr_loader_generic.so": "a" * 64}}})
|
|
adapter_manifest.unlink()
|
|
assert controllers.apply_default(target, Reporter())["needs_update"] == [game["package"]]
|
|
|
|
|
|
def test_live_suggestion_push_uses_inherited_value(monkeypatch):
|
|
library.set_setting("input.quest_buttons", 2)
|
|
monkeypatch.setattr(pipeline, "apply_suggestions", lambda *a: Recipe("test.quest"))
|
|
target = Target([])
|
|
assert pipeline.apply_suggestions_live("test.quest", ["adapter.scale=1"], target)
|
|
assert target.calls[0][1]["quest_buttons"] == 2
|
|
|
|
|
|
def test_settings_apply_uses_frame_job_queue_and_selected_default(adapter_manifest):
|
|
game = store()
|
|
library.set_fields(game["package"], build={"sha256": "apk", "build_inputs": {"native": {
|
|
"arm64-v8a/libopenxr_loader_generic.so": "a" * 64}}})
|
|
target, jobs = Target([{"package": game["package"], "sha256": "apk"}, {"package": "old"}]), []
|
|
app = SimpleNamespace(frame_state="connected", target=target,
|
|
submit=lambda *a, **k: jobs.append((a, k)), toast=lambda *a: None)
|
|
controls = list(walk(SettingsView(app).controllers()))
|
|
choice = next(c for c in controls if isinstance(c, ft.Dropdown))
|
|
choice.on_select(SimpleNamespace(control=SimpleNamespace(value="1")))
|
|
apply = next(c for c in controls if getattr(c, "content", None) == "Apply to installed games now")
|
|
apply.on_click(None)
|
|
args, kwargs = jobs[0]
|
|
assert kwargs["kind"] == "tool-frame" and not target.calls
|
|
message = args[1](SimpleNamespace(reporter=Reporter()))
|
|
assert "Applied to 1 game." in message and "1 game needs Update on Frame first." in message
|
|
assert target.calls == [(game["package"], {"quest_buttons": 1}), ("old", {"quest_buttons": 1})]
|
|
|
|
|
|
def test_controller_section_reads_default_once_and_updates_preview(monkeypatch):
|
|
reads = []
|
|
def snapshot():
|
|
reads.append(True)
|
|
assert len(reads) == 1
|
|
return 1
|
|
monkeypatch.setattr(controllers, "quest_buttons_default", snapshot)
|
|
controls = list(walk(SettingsView(SimpleNamespace(frame_state="none")).controllers()))
|
|
choice = next(c for c in controls if isinstance(c, ft.Dropdown))
|
|
preview = next(c for c in controls if isinstance(c, ft.Image))
|
|
assert choice.value == "1" and preview.src.endswith("quest-buttons-1.svg")
|
|
choice.on_select(SimpleNamespace(control=SimpleNamespace(value="2")))
|
|
assert preview.src.endswith("quest-buttons-2.svg") and len(reads) == 1
|
|
assert library.setting("input.quest_buttons") == 2
|
|
|
|
|
|
def test_generated_controller_assets_are_current():
|
|
root = Path(__file__).resolve().parents[1]
|
|
subprocess.run([sys.executable, str(root / "scripts/controller_diagram.py"), "--check"], check=True)
|