Files
spoopyghosty0--frameport/tests/test_library_folders.py
T
spoopyghosty0 c67f79ebc6 Install-link update: take the new download instead of failing ('couldn't be read as an Android app')
Updating a game from its install link downloads the new APK into its own folder; the scan then finds it as another version of a library game (#351 relinking), switches the entry to it and returns nothing, and add_from_link reported that as an unreadable APK, so the update stopped before the link version was recorded. add_from_link now takes the entry from the scan report; if the scan left the library's copy (still on disk), the link switches the game to the download like Update with a newer APK.
2026-10-18 08:54:23 -04:00

282 lines
13 KiB
Python

"""GitHub discussion #351: the library folders list (Settings → Library folders, `frameport folders`) and scans that
find games of the library at a new place (moved: relinked in place; another version; another copy)."""
import shutil
import zipfile
from typer.testing import CliRunner
from frameport import cli, pipeline
from frameport.artwork import fetch as artwork
from frameport.core import folders, library
from frameport.core.events import Reporter
from frameport.patches import base
from frameport.sources import quest_dump
PKG = "com.example.questgame"
# ------------------------------------------------------------------------------------------ the folder list
def test_folder_list_add_remove_status(tmp_path):
a, b = tmp_path / "A", tmp_path / "B"
a.mkdir()
b.mkdir()
folders.remember(a)
folders.remember(b)
folders.remember(a) # scanned again: once, moved to the end
assert folders.roots() == [str(b), str(a)]
assert folders.add(b) == [str(b), str(a)] # already there: unchanged
library.upsert_game("g1", apk=str(a / "x" / "g.apk"))
library.upsert_game("g2", kind="rift", game_dir=str(a / "Game"))
library.upsert_game("g3", apk=str(tmp_path / "elsewhere.apk"))
shutil.rmtree(b)
st = {s.path: s for s in folders.status()}
assert (st[str(a)].exists, st[str(a)].games) == (True, 2)
assert (st[str(b)].exists, st[str(b)].games) == (False, 0)
assert folders.rescan_roots() == [str(a)] # a missing folder isn't scanned
assert folders.remove(str(a)) is True and folders.remove(str(a)) is False
assert library.game("g1") and library.game("g2") # its games stay
try:
folders.add(tmp_path / "nope")
raise AssertionError("a missing folder can't be added")
except ValueError:
pass
def test_rescan_roots_fallback_only_before_anything_was_remembered(tmp_path):
games = tmp_path / "Games"
(games / "One").mkdir(parents=True)
library.upsert_game("g1", apk=str(games / "One" / "g.apk"), origin=str(games / "One"))
assert folders.rescan_roots() == [str(games)] # an older library: where its games came from
folders.remember(games)
folders.remember(games / "One") # inside another one: scanned with it
assert folders.rescan_roots() == [str(games)]
folders.remove(games)
folders.remove(games / "One")
assert folders.rescan_roots() == [] # the user removed them all: no guessing them back
def test_cli_folders(tmp_path):
d = tmp_path / "Games"
d.mkdir()
runner = CliRunner()
res = runner.invoke(cli.app, ["folders", "--add", str(d)])
assert res.exit_code == 0 and str(d) in res.output and "ok" in res.output
assert folders.roots() == [str(d)]
res = runner.invoke(cli.app, ["folders", "--json"])
assert '"exists": true' in res.output
res = runner.invoke(cli.app, ["folders", "--remove", str(d)])
assert res.exit_code == 0 and folders.roots() == []
assert runner.invoke(cli.app, ["folders", "--remove", str(d)]).exit_code == 1
assert runner.invoke(cli.app, ["folders", "--add", str(tmp_path / "missing")]).exit_code != 0
assert "no folders to rescan" in runner.invoke(cli.app, ["rescan"]).output
# ------------------------------------------------------------------------------------------ Quest games that moved
def _apk(path, manifest, extra=b""):
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as z:
z.writestr("AndroidManifest.xml", manifest)
z.writestr("classes.dex", b"dex\n035\0" + extra)
return path
def _game(root, manifest, extra=b""):
apk = _apk(root / "Quest Game" / "game.apk", manifest, extra)
obb = root / "Quest Game" / PKG
obb.mkdir()
(obb / f"main.1.{PKG}.obb").write_bytes(b"o" * 10)
return apk
def _setup(tmp_path, monkeypatch, manifest):
base.load_all()
monkeypatch.setattr(quest_dump, "_apk_ids", lambda p: (PKG, 1))
monkeypatch.setattr(pipeline.rift_dump, "scan", lambda *a, **k: [])
_game(tmp_path / "Old", manifest)
[g] = pipeline.add_path(tmp_path / "Old")
r = library.recipe_from_dict(g["recipe"])
r.source, r.status = "user", "works"
r.patches["adapter.scale"] = {"value": 1.2}
library.upsert_game(PKG, title="My Game", title_locked=True, tags=["mine"], recipe=library.recipe_to_dict(r),
status="works", build={"ok": True, "sha256": "b"}, installs={"Frame": {"time": 1}})
art = artwork.artwork_dir(PKG)
(art / artwork.PICKED).write_text("custom")
return g
def _logs():
logs = []
rep = Reporter([lambda e: logs.append(e.message) if e.kind == "log" else None])
return rep, logs
def _kept_user_fields(g):
assert (g["title"], g["title_locked"], g["tags"]) == ("My Game", True, ["mine"])
assert g["recipe"]["source"] == "user" and g["recipe"]["patches"]["adapter.scale"] == {"value": 1.2}
assert g["status"] == "works" and g["build"] == {"ok": True, "sha256": "b"} and g["installs"]
assert (artwork.artwork_dir(PKG) / artwork.PICKED).is_file()
def test_rescan_relinks_a_moved_quest_game(tmp_path, monkeypatch, quest_manifest):
_setup(tmp_path, monkeypatch, quest_manifest)
shutil.move(tmp_path / "Old", tmp_path / "New") # e.g. another drive
rep, logs = _logs()
report = pipeline.ScanReport()
assert pipeline.add_path(tmp_path / "New", rep, only_new=True, report=report) == []
g = library.game(PKG)
assert [i["package"] for i in report.moved] == [PKG]
new = tmp_path / "New" / "Quest Game"
assert g["apk"] == str(new / "game.apk") and g["data_dir"] == str(new / PKG) and g["origin"] == str(new)
assert g["data_manifest"] == {f"main.1.{PKG}.obb": 10}
_kept_user_fields(g)
assert any("aren't at" in m and "drive" in m for m in logs) # the old place is gone (maybe unplugged)
assert any("Updated the location of 1 game" in m for m in logs)
pipeline.check_source(g) # buildable again
def test_full_scan_keeps_a_user_recipe_and_refreshes_a_derived_one(tmp_path, monkeypatch, quest_manifest):
g0 = _setup(tmp_path, monkeypatch, quest_manifest)
[g] = pipeline.add_path(tmp_path / "Old") # scanned again, same place
_kept_user_fields(g)
assert g["suggested"] == g0["suggested"]
stale = dict(g0["recipe"], patches={}, source="heuristics")
library.upsert_game(PKG, recipe=stale)
[g] = pipeline.add_path(tmp_path / "Old")
assert g["recipe"] == g["suggested"] # not the user's: follows the analysis
def test_another_version_isnt_swapped_silently(tmp_path, monkeypatch, quest_manifest):
_setup(tmp_path, monkeypatch, quest_manifest)
shutil.rmtree(tmp_path / "Old")
newer = _game(tmp_path / "New", quest_manifest, extra=b"v2") # other bytes: another build
report = pipeline.ScanReport()
pipeline.add_path(tmp_path / "New", only_new=True, report=report) # Rescan folders: the user decides
assert [i["package"] for i in report.other_version] == [PKG] and not report.moved
assert library.game(PKG)["apk"] == str(tmp_path / "Old" / "Quest Game" / "game.apk")
report = pipeline.ScanReport()
assert pipeline.add_path(tmp_path / "New", report=report) == [] # Scan a folder…: like Update with a newer APK…
g = library.game(PKG)
assert [i["package"] for i in report.updated] == [PKG]
assert g["apk"] == str(newer) and g["source_sha256"] == pipeline.builder.sha256(newer)
assert (g["title"], g["tags"], g["recipe"]["source"]) == ("My Game", ["mine"], "user")
def test_an_install_link_update_takes_the_new_download(tmp_path, monkeypatch, quest_manifest):
"""Updating a game from its install link: the scan sees the new download as another version of a library game
(and returns nothing); add_from_link must still answer with the game, now on the new file."""
from frameport import deeplink
_setup(tmp_path, monkeypatch, quest_manifest)
m = deeplink.Manifest("My Game", [deeplink.ManifestFile("https://a.com/game-2.apk")], "https://a.com/game.json",
version="2")
# the earlier download is gone (FramePort removes it), the new one arrives in its own folder
shutil.rmtree(tmp_path / "Old")
newer = _game(tmp_path / "Dl2", quest_manifest, extra=b"v2")
g = pipeline.add_from_link(m, newer)
assert g["package"] == PKG and g["apk"] == str(newer) and g["link"]["version"] == "2"
assert (g["tags"], g["recipe"]["source"]) == (["mine"], "user")
# the earlier download still on disk: the link still switches the game to the new file
newest = _game(tmp_path / "Dl3", quest_manifest, extra=b"v3")
g = pipeline.add_from_link(m, newest)
assert g["apk"] == str(newest) and g["source_sha256"] == pipeline.builder.sha256(newest)
def test_a_second_copy_leaves_the_existing_one(tmp_path, monkeypatch, quest_manifest):
_setup(tmp_path, monkeypatch, quest_manifest)
_game(tmp_path / "Copy", quest_manifest)
for only_new in (True, False):
report = pipeline.ScanReport()
assert pipeline.add_path(tmp_path / "Copy", only_new=only_new, report=report) == []
assert [i["package"] for i in report.other_copy] == [PKG]
g = library.game(PKG)
assert g["apk"] == str(tmp_path / "Old" / "Quest Game" / "game.apk")
_kept_user_fields(g)
def test_same_build_without_a_checksum_uses_the_version_name(tmp_path, monkeypatch):
apk = tmp_path / "x.apk"
apk.write_bytes(b"x")
monkeypatch.setattr(quest_dump, "version_name", lambda p: "1.2")
assert pipeline.same_quest_build({"analysis": {"version": "1.2"}}, apk)
assert not pipeline.same_quest_build({"analysis": {"version": "1.1"}}, apk)
assert not pipeline.same_quest_build({"source_sha256": "0" * 64, "analysis": {"version": "1.2"}}, apk)
def test_scan_summary_lines():
from frameport.ui.app import scan_report_lines
report = pipeline.ScanReport()
assert scan_report_lines(report) == []
report.note("moved", {"package": "a", "title": "A"}, "/x")
report.note("moved", {"package": "b", "title": "B"}, "/y")
report.note("other_version", {"package": "c"}, "/z")
lines = scan_report_lines(report)
assert lines[0] == "Updated the location of 2 games" and "another version" in lines[1]
# ------------------------------------------------------------------------------------------ PC VR games that moved
def _rift(tmp_path):
from test_rift import unity_game
unity_game(tmp_path / "Old")
[g] = pipeline.add_path(tmp_path / "Old")
pkg = g["package"]
r = library.recipe_from_dict(g["recipe"])
r.source = "user"
r.patches["pcvr.no_crash_reporter"] = {}
pipeline.set_recipe(pkg, r)
library.upsert_game(pkg, title="My Space", title_locked=True, tags=["mine"], exe_confirmed=True)
return pkg
def _rift_kept(g):
assert (g["title"], g["tags"], g["exe"]) == ("My Space", ["mine"], "Space Game.exe")
assert g["recipe"]["source"] == "user" and "pcvr.no_crash_reporter" in g["recipe"]["patches"]
def test_rescan_relinks_a_moved_rift_game(tmp_path):
pkg = _rift(tmp_path)
(tmp_path / "New").mkdir()
shutil.move(tmp_path / "Old" / "Space Game", tmp_path / "New" / "Space Game")
report = pipeline.ScanReport()
assert pipeline.add_path(tmp_path / "New", only_new=True, report=report) == []
g = library.game(pkg)
new = tmp_path / "New" / "Space Game"
assert [i["package"] for i in report.moved] == [pkg]
assert g["game_dir"] == str(new) and g["analysis"]["extra"]["folder"] == str(new)
_rift_kept(g)
assert len([x for x in library.games() if x.get("kind") == "rift"]) == 1 # no second entry
def test_a_renamed_rift_folder_is_found_by_its_program(tmp_path):
pkg = _rift(tmp_path)
(tmp_path / "New").mkdir()
shutil.move(tmp_path / "Old" / "Space Game", tmp_path / "New" / "Space Game (repack)")
report = pipeline.ScanReport()
pipeline.add_path(tmp_path / "New", only_new=True, report=report)
assert [i["package"] for i in report.moved] == [pkg]
assert library.game(pkg)["game_dir"] == str(tmp_path / "New" / "Space Game (repack)")
assert len([x for x in library.games() if x.get("kind") == "rift"]) == 1
def test_rift_copy_and_version(tmp_path):
pkg = _rift(tmp_path)
shutil.copytree(tmp_path / "Old" / "Space Game", tmp_path / "Copy" / "Space Game")
report = pipeline.ScanReport()
pipeline.add_path(tmp_path / "Copy", report=report) # the library's folder is still there: it stays
assert [i["package"] for i in report.other_copy] == [pkg]
assert library.game(pkg)["game_dir"] == str(tmp_path / "Old" / "Space Game")
shutil.rmtree(tmp_path / "Old")
exe = tmp_path / "Copy" / "Space Game" / "Space Game.exe"
exe.write_bytes(exe.read_bytes() + b"\0" * 512) # another build
report = pipeline.ScanReport()
pipeline.add_path(tmp_path / "Copy", only_new=True, report=report)
assert [i["package"] for i in report.other_version] == [pkg]
assert library.game(pkg)["game_dir"] == str(tmp_path / "Old" / "Space Game")
report = pipeline.ScanReport()
pipeline.add_path(tmp_path / "Copy", report=report)
g = library.game(pkg)
assert [i["package"] for i in report.updated] == [pkg] and g["source_id"]
assert g["game_dir"] == str(tmp_path / "Copy" / "Space Game")
_rift_kept(g)