Files
spoopyghosty0--frameport/tests/test_controller_preferences.py
T

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)