Add BeatMods parsing and userdata restore
This commit is contained in:
+155
-1
@@ -10,6 +10,7 @@ from unittest.mock import patch
|
||||
from zipfile import ZipFile
|
||||
|
||||
from plugin_helper.bootstrap import _run_ipa
|
||||
from plugin_helper.beatmods import by_version_id, normalize_mods
|
||||
from plugin_helper.checker import check_lock
|
||||
from plugin_helper.cli import installed_plugins_report, run
|
||||
from plugin_helper.fsutil import sha256_file
|
||||
@@ -20,10 +21,67 @@ from plugin_helper.planner import create_plan
|
||||
from plugin_helper.scanner import scan_bootstrap_files, scan_instance
|
||||
from plugin_helper.state import downloads_dir, load_installed_state, plugin_downloads_dir, save_bootstrap_state
|
||||
from plugin_helper.updates import check_updates
|
||||
from plugin_helper.userdata import backup_userdata, infer_windows_appdata_path, sync_windows_data_repo
|
||||
from plugin_helper.userdata import (
|
||||
backup_userdata,
|
||||
infer_appdata_path,
|
||||
infer_proton_appdata_path,
|
||||
infer_windows_appdata_path,
|
||||
restore_windows_data_repo,
|
||||
sync_windows_data_repo,
|
||||
)
|
||||
|
||||
|
||||
class PluginHelperTests(unittest.TestCase):
|
||||
def test_normalize_beatmods_current_nested_response(self) -> None:
|
||||
payload = {
|
||||
"mods": [
|
||||
{
|
||||
"mod": {
|
||||
"id": 10,
|
||||
"name": "Example",
|
||||
"gitUrl": "https://github.com/example/mod",
|
||||
"category": "library",
|
||||
},
|
||||
"latest": {
|
||||
"id": 1234,
|
||||
"modVersion": "1.2.3",
|
||||
"zipHash": "abc123",
|
||||
"dependencies": [2561, {"id": "2567"}],
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
entries = normalize_mods(payload)
|
||||
|
||||
self.assertEqual(len(entries), 1)
|
||||
self.assertEqual(entries[0].name, "Example")
|
||||
self.assertEqual(entries[0].mod_id, 10)
|
||||
self.assertEqual(entries[0].version_id, 1234)
|
||||
self.assertEqual(entries[0].dependencies, (2561, 2567))
|
||||
self.assertEqual(by_version_id(entries)[1234].zip_hash, "abc123")
|
||||
|
||||
def test_normalize_beatmods_legacy_flat_response(self) -> None:
|
||||
entries = normalize_mods(
|
||||
[
|
||||
{
|
||||
"id": "12",
|
||||
"name": "FlatExample",
|
||||
"gitUrl": "",
|
||||
"category": "mods",
|
||||
"modVersion": "2.0.0",
|
||||
"zipHash": "def456",
|
||||
"dependencies": [{"id": 44}, "45", None],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(entries[0].name, "FlatExample")
|
||||
self.assertEqual(entries[0].mod_id, 12)
|
||||
self.assertEqual(entries[0].version_id, 12)
|
||||
self.assertIsNone(entries[0].git_url)
|
||||
self.assertEqual(entries[0].dependencies, (44, 45))
|
||||
|
||||
def test_instances_and_scan(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
@@ -480,6 +538,102 @@ class PluginHelperTests(unittest.TestCase):
|
||||
self.assertIn("BeatLeader/Replays", descriptor["skipped"])
|
||||
self.assertIn("*.log", descriptor["excludePatterns"])
|
||||
|
||||
def test_infer_proton_appdata_path(self) -> None:
|
||||
self.assertEqual(
|
||||
infer_proton_appdata_path(),
|
||||
Path.home()
|
||||
/ ".local/share/BSManager/SharedContent/compatdata/pfx/drive_c/users/steamuser/AppData/LocalLow/Hyperbolic Magnetism/Beat Saber",
|
||||
)
|
||||
|
||||
def test_infer_appdata_path_uses_windows_or_proton(self) -> None:
|
||||
windows_instance = Path("/home/pleb/Windows/Users/pleb/BSManager/BSInstances/1.44.1")
|
||||
linux_instance = Path("/home/pleb/.local/share/BSManager/BSInstances/1.44.1")
|
||||
|
||||
self.assertEqual(infer_appdata_path(windows_instance), infer_windows_appdata_path(windows_instance))
|
||||
self.assertEqual(infer_appdata_path(linux_instance), infer_proton_appdata_path())
|
||||
|
||||
def test_restore_windows_data_repo_roundtrip(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
instance = root / "Users" / "pleb" / "BSManager" / "BSInstances" / "1.44.1"
|
||||
appdata = root / "Users" / "pleb" / "AppData" / "LocalLow" / "Hyperbolic Magnetism" / "Beat Saber"
|
||||
backup_repo = root / "backup"
|
||||
(instance / "UserData").mkdir(parents=True)
|
||||
(instance / "UserData" / "settings.json").write_text('{"saved": true}', encoding="utf-8")
|
||||
appdata.mkdir(parents=True)
|
||||
(appdata / "settings.cfg").write_text("settings", encoding="utf-8")
|
||||
|
||||
sync_windows_data_repo(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
backup_root=backup_repo,
|
||||
)
|
||||
|
||||
(instance / "UserData" / "settings.json").write_text('{"saved": false}', encoding="utf-8")
|
||||
(appdata / "settings.cfg").write_text("changed", encoding="utf-8")
|
||||
|
||||
result = restore_windows_data_repo(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
backup_root=backup_repo,
|
||||
)
|
||||
|
||||
self.assertEqual((instance / "UserData" / "settings.json").read_text(), '{"saved": true}')
|
||||
self.assertEqual((appdata / "settings.cfg").read_text(), "settings")
|
||||
self.assertEqual(len(result["restored"]), 2)
|
||||
self.assertTrue(all(item["snapshot"] for item in result["restored"]))
|
||||
|
||||
def test_restore_windows_data_repo_rejects_instance_mismatch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
instance = root / "1.44.1"
|
||||
backup_repo = root / "backup"
|
||||
(instance / "UserData").mkdir(parents=True)
|
||||
(instance / "UserData" / "settings.json").write_text("{}", encoding="utf-8")
|
||||
(backup_repo / "UserData").mkdir(parents=True)
|
||||
(backup_repo / "UserData" / "settings.json").write_text("{}", encoding="utf-8")
|
||||
(backup_repo / "backup-descriptor.json").write_text(
|
||||
json.dumps({"instance": "1.40.8"}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "does not match"):
|
||||
restore_windows_data_repo(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
backup_root=backup_repo,
|
||||
include_appdata=False,
|
||||
)
|
||||
|
||||
def test_restore_userdata_cli(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
instances_root = root / "instances"
|
||||
instance = instances_root / "1.44.1"
|
||||
backup_repo = root / "backup"
|
||||
(instance / "UserData").mkdir(parents=True)
|
||||
(instance / "UserData" / "settings.json").write_text('{"restored": true}', encoding="utf-8")
|
||||
(backup_repo / "UserData").mkdir(parents=True)
|
||||
(backup_repo / "UserData" / "settings.json").write_text('{"restored": true}', encoding="utf-8")
|
||||
|
||||
status = run(
|
||||
[
|
||||
"--instances-root",
|
||||
str(instances_root),
|
||||
"--state-dir",
|
||||
str(root / "state"),
|
||||
"restore-userdata",
|
||||
"--instance",
|
||||
"1.44.1",
|
||||
"--backup-root",
|
||||
str(backup_repo),
|
||||
"--no-appdata",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual((instance / "UserData" / "settings.json").read_text(), '{"restored": true}')
|
||||
|
||||
def test_check_reports_missing_asset(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user