Files
spoopyghosty0--frameport/tests/test_link_updates.py
T
spoopyghosty0 4e8486418f Install links: Check for updates on the game page; links without a version are checked too
Games installed from an install link get a row on the game page (where it came from, the installed version) with Check for updates, and the same entry in the page's menu: it asks the link now instead of waiting for the 6-hourly check. Manifests that named no version (e.g. an app's first release) were never checked; the installed APK's own versionName (from the analysis) is now the version to compare with, in the automatic check, the button and frameport link-updates.
2026-10-18 09:47:02 -04:00

187 lines
9.0 KiB
Python

"""Install-link updates: a manifest's `version`, remembered with the game and checked again at start."""
import json
import pytest
from frameport import deeplink, pipeline
from frameport.core import library
APK = "https://cdn.example.com/game-arm64.apk"
SRC = "https://cdn.example.com/game.json"
def _data(**kw):
return {"schema": deeplink.SCHEMA, "name": "G", "files": [{"url": APK}], **kw}
@pytest.mark.parametrize("data,version", [
(_data(version="1.4.2"), "1.4.2"),
(_data(version=3), "3"),
(_data(frameport={"version": " 2.0 "}), "2.0"),
(_data(version="1.0", frameport={"version": "2.0"}), "2.0"),
(_data(version=True), ""), (_data(version={"x": 1}), ""), (_data(version="x" * 100), ""), (_data(), ""),
])
def test_manifest_version(data, version):
assert deeplink.manifest_from_data(data, SRC).version == version
@pytest.mark.parametrize("a,b", [
("1.10", "1.9"), ("v2", "1.99.99"), ("1.2.0", "1.2.0-beta3"), ("1.2.0-rc10", "1.2.0-rc2"), ("1.2.1", "1.2"),
("1.0.1", "1.0.0b"),
])
def test_newer_versions(a, b):
assert deeplink.compare_versions(a, b) == 1 and deeplink.compare_versions(b, a) == -1
@pytest.mark.parametrize("a,b", [("1.2", "1.2.0"), ("v1.2", "1.2"), ("build 7", "build 7")])
def test_equal_versions(a, b):
assert deeplink.compare_versions(a, b) == 0
def test_mixed_versions_dont_crash():
assert deeplink.compare_versions("1.0-2", "1.0-b") in (-1, 1)
assert deeplink.compare_versions("nightly", "1.0") in (-1, 1)
assert not deeplink.is_newer_version("2.0", "") and not deeplink.is_newer_version("", "1.0")
@pytest.mark.parametrize("link,ok", [
({"source": SRC, "version": "1.0"}, True),
({"source": SRC, "version": ""}, False),
({"source": SRC, "version": "1.0", "direct": True}, False),
({"source": APK, "version": "1.0"}, False), # a bare file link
({"source": "/home/u/game.json", "version": "1.0"}, False),
({"source": deeplink.DEMO_MANIFEST, "version": "1.0"}, False),
({"source": "https://10.0.0.2/game.json", "version": "1.0"}, False),
(None, False),
])
def test_which_links_are_checked(link, ok):
assert deeplink.can_check_updates(link) is ok
def test_links_without_a_version_use_the_installed_one():
"""A manifest that named no version (e.g. a first release) is still checked: against the installed APK's own
versionName. The game page's "Check for updates" needs only a manifest it can fetch again."""
link = {"source": SRC, "version": ""}
assert not deeplink.can_check_updates(link) and deeplink.can_check_updates(link, "0.1.0")
assert deeplink.can_check_link(link) and not deeplink.can_check_link({"source": APK, "version": ""})
assert not deeplink.can_check_link({"source": SRC, "direct": True})
g = {"package": "com.x", "apk": "/x.apk", "link": link, "analysis": {"version": "0.1.0"}}
assert pipeline.link_installed_version(g) == "0.1.0"
assert pipeline.link_installed_version({**g, "link": {"source": SRC, "version": "0.2"}}) == "0.2"
assert pipeline.link_installed_version({**g, "kind": "linux"}) == ""
def test_check_finds_an_update_for_a_link_without_a_version(monkeypatch, tmp_path):
library.upsert_game("com.x", title="X", apk=str(tmp_path / "x.apk"), analysis={"version": "0.1.0"},
link={"source": SRC, "name": "X", "version": "", "direct": False})
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data(
_data(version="0.2.1"), req.manifest_url))
assert pipeline.check_link_updates(force=True) == ["com.x"]
assert library.game("com.x")["link_update"]["current"] == "0.1.0"
def _add(monkeypatch, tmp_path, version, name="G"):
path = tmp_path / "Tool.exe"
path.write_bytes(b"MZ")
if not library.game("rift.tool"):
library.upsert_game("rift.tool", kind="rift", details={"sources": []})
monkeypatch.setattr(pipeline, "add_windows_exe", lambda p, r=None: library.game("rift.tool"))
m = deeplink.manifest_from_data(_data(name=name, version=version, files=[{"url": "https://c.example.com/T.exe"}]),
SRC)
return pipeline.add_from_link(m, path)
def test_check_marks_and_clears_updates(monkeypatch, tmp_path):
g = _add(monkeypatch, tmp_path, "1.0")
assert g["link"]["version"] == "1.0" and g["link"]["direct"] is False
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data(
_data(version="1.1"), req.manifest_url))
assert pipeline.check_link_updates(force=True) == ["rift.tool"]
assert library.game("rift.tool")["link_update"]["version"] == "1.1"
# within LINK_CHECK_HOURS nothing is fetched, the known offer stays
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("fetched too early"))
assert pipeline.check_link_updates() == ["rift.tool"]
# installing the update records its version and clears the offer
g = _add(monkeypatch, tmp_path, "1.1")
assert g["link"]["version"] == "1.1" and "link_update" not in g
# an offer the manifest withdrew goes away
library.upsert_game("rift.tool", link_update={"version": "9"})
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: deeplink.manifest_from_data(_data(version="1.1"), SRC))
assert pipeline.check_link_updates(force=True) == []
# a manifest that can't be read keeps the state
def offline(req):
raise OSError("offline")
library.upsert_game("rift.tool", link_update={"version": "1.2"})
monkeypatch.setattr(deeplink, "fetch_manifest", offline)
assert pipeline.check_link_updates(force=True) == ["rift.tool"]
def test_check_can_be_switched_off(monkeypatch, tmp_path):
_add(monkeypatch, tmp_path, "1.0")
library.set_setting("links.check_updates", False)
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("checked while off"))
assert pipeline.check_link_updates() == []
def test_update_keeps_the_users_own_name(monkeypatch, tmp_path):
_add(monkeypatch, tmp_path, "1.0", name="Tool")
library.upsert_game("rift.tool", title="My Tool", title_locked=True)
assert _add(monkeypatch, tmp_path, "1.1", name="Tool")["title"] == "My Tool"
library.upsert_game("rift.tool", title="Tool") # not renamed: a new name from the manifest is taken
assert _add(monkeypatch, tmp_path, "1.2", name="Tool 2")["title"] == "Tool 2"
def test_demo_link_is_never_checked_for_updates(monkeypatch):
"""The homepage's example button stays a demo: its manifest names a version, but nothing about it is fetched."""
from pathlib import Path
site = json.loads((Path(__file__).parents[1] / "site/public/demo/cool-game.json").read_text())
assert deeplink.manifest_from_data(site, deeplink.DEMO_MANIFEST).version == deeplink.demo_manifest().version
assert deeplink.parse(deeplink.make_link(deeplink.DEMO_MANIFEST)).demo
assert not deeplink.can_check_updates({"source": deeplink.DEMO_MANIFEST, "version": "0.1"})
library.upsert_game("demo.like", link={"source": deeplink.DEMO_MANIFEST, "version": "0.1"})
monkeypatch.setattr(deeplink, "fetch_manifest", lambda req: pytest.fail("the demo was fetched"))
assert pipeline.check_link_updates(force=True) == []
def test_local_server_round_trip(tmp_path):
"""check_update fetches the remembered manifest over HTTP (loopback is allowed for testing)."""
import threading
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
class Quiet(SimpleHTTPRequestHandler):
def log_message(self, *args):
pass
(tmp_path / "game.json").write_text(json.dumps(_data(version="2.0")))
httpd = ThreadingHTTPServer(("127.0.0.1", 0), partial(Quiet, directory=str(tmp_path)))
threading.Thread(target=httpd.serve_forever, daemon=True).start()
try:
link = {"source": f"http://127.0.0.1:{httpd.server_address[1]}/game.json", "version": "1.0"}
assert deeplink.can_check_updates(link)
assert deeplink.check_update(link).version == "2.0"
assert deeplink.check_update({**link, "version": "2.0"}) is None
finally:
httpd.shutdown()
def test_update_toast_has_an_update_button(monkeypatch):
"""The toast that announces a newer version starts the update itself (it used to offer no way to get it)."""
from frameport.ui import app as app_mod
from frameport.ui.views import link_dialog
toasts, started = [], []
a = app_mod.FramePortApp.__new__(app_mod.FramePortApp)
a.route = ("library",)
monkeypatch.setattr(a, "refresh_view", lambda: None, raising=False)
monkeypatch.setattr(a, "_title", lambda pkg: pkg, raising=False)
monkeypatch.setattr(a, "toast", lambda msg, **k: toasts.append(k), raising=False)
monkeypatch.setattr(library, "games", lambda: [])
monkeypatch.setattr(pipeline, "check_link_updates", lambda force=False: ["linux.tool"])
monkeypatch.setattr(link_dialog, "update_from_link", lambda app, pkgs: started.append(pkgs))
a._check_link_updates()
assert toasts and toasts[0]["action"]
toasts[0]["on_action"](None)
assert started == [["linux.tool"]]