summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/steamLaunchOptions.test.ts168
-rw-r--r--tests/test_configuration_profiles.py80
-rw-r--r--tests/test_flatpak_profile_service.py210
-rw-r--r--tests/test_flatpak_service.py419
-rw-r--r--tests/test_plugin_migration.py72
-rw-r--r--tests/test_steam_service.py99
-rw-r--r--tests/test_wrapper_service.py149
7 files changed, 665 insertions, 532 deletions
diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts
index c799907..40aeb3c 100644
--- a/tests/steamLaunchOptions.test.ts
+++ b/tests/steamLaunchOptions.test.ts
@@ -28,7 +28,7 @@ test("inserts one wrapper immediately before an existing command macro", () => {
});
});
-test("normalizes blank and argument-only fields while refusing ambiguous launchers", () => {
+test("normalizes blank and argument-only shortcut fields", () => {
assert.deepEqual(installWrapperLaunchOption("", wrapper), {
options: `${wrapper} %command%`,
commandTokenAdded: true,
@@ -41,7 +41,7 @@ test("normalizes blank and argument-only fields while refusing ambiguous launche
assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/);
});
-test("preserves assignments, quoting, suffixes, and unrelated values", () => {
+test("preserves assignments quoting suffixes and released wrapper cleanup", () => {
const options = 'FOO="hello world" VK_INSTANCE_LAYERS="one:two" gamemoderun %command% --flag "two words"';
assert.equal(
installWrapperLaunchOption(options, wrapper).options,
@@ -49,50 +49,40 @@ test("preserves assignments, quoting, suffixes, and unrelated values", () => {
);
assert.equal(removeWrapperLaunchOption(`${wrapper} %command% --arg "${wrapper}"`, wrapper, true), `--arg "${wrapper}"`);
assert.equal(normalizeLaunchOptions(" FOO=bar %COMMAND% --flag "), "FOO=bar %COMMAND% --flag");
-});
-
-test("cleans current, legacy, and bare Mako wrappers without touching suffix arguments", () => {
for (const token of ["~/lsfg", "/home/deck/lsfg", "mako-run", "mako-launch"]) {
assert.equal(cleanupLegacyWrapper(`FOO=bar ${token} %command% --arg "${token}"`), `FOO=bar %command% --arg "${token}"`);
}
- assert.equal(cleanupLegacyWrapper(`FOO=bar ${wrapper} %command%`), "FOO=bar %command%");
assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), true);
assert.equal(isLegacyWrapperToken("/opt/tools/lsfg"), false);
- assert.equal(removeWrapperLaunchOption(`FOO=bar ${wrapper} %command% --arg`, wrapper), "FOO=bar %command% --arg");
});
-test("removes only old plugin assignments and preserves DXVK settings", () => {
+test("removes only managed assignments and preserves unrelated values", () => {
assert.equal(
cleanupPluginAssignments(
'FOO="keep this" ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" %command%',
),
'FOO="keep this" DXVK_CONFIG="dxgi.syncInterval = 0" %command%',
);
- assert.equal(
- cleanupPluginLaunchOptions(`DXVK_FRAME_RATE=30 ${wrapper} %command%`, wrapper),
- "%command%",
- );
+ assert.equal(cleanupPluginLaunchOptions(`DXVK_FRAME_RATE=30 ${wrapper} %command%`, wrapper), "%command%");
assert.equal(
cleanupPluginAssignments("PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%"),
"PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%",
);
});
-test("reads the matching app-details field and installs/removes Steam integration", async () => {
+test("uses launch options for Steam and non-Steam shortcuts without a Target API", async () => {
const previousWindow = (globalThis as Record<string, unknown>).window;
const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient;
let appOptions = "FOO=bar %command%";
let shortcutOptions = "--windowed";
- let shortcutTarget = '"flatpak"';
const appWrites: string[] = [];
const shortcutWrites: string[] = [];
- const targetWrites: string[] = [];
const unregisters: number[] = [];
const apps = {
RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) {
callback(appId === 42
? { strLaunchOptions: appOptions, strShortcutLaunchOptions: "wrong-field" }
- : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" });
+ : { strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" });
return { unregister: () => unregisters.push(appId) };
},
SetAppLaunchOptions(appId: number, options: string) {
@@ -105,11 +95,6 @@ test("reads the matching app-details field and installs/removes Steam integratio
shortcutWrites.push(options);
shortcutOptions = options;
},
- SetShortcutExe(appId: number, executable: string) {
- assert.equal(appId, 43);
- targetWrites.push(executable);
- shortcutTarget = executable;
- },
};
(globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout };
(globalThis as Record<string, unknown>).SteamClient = { Apps: apps };
@@ -120,18 +105,15 @@ test("reads the matching app-details field and installs/removes Steam integratio
assert.equal(installed.snapshot.options, `FOO=bar ${wrapper} %command%`.replaceAll(" ", " "));
assert.equal(installed.commandTokenAdded, false);
assert.equal(appWrites.length, 1);
- assert.equal(shortcutWrites.length, 0);
- const shortcut = await installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" });
- assert.equal(shortcut.originalExecutable, "/usr/bin/flatpak");
- assert.equal(shortcut.snapshot.target, wrapper);
- assert.deepEqual(targetWrites, [wrapper]);
- const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, { kind: "flatpak", flatpakAppId: "com.example.Game" });
- assert.equal(restored.target, "/usr/bin/flatpak");
- assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]);
- assert.equal(shortcutWrites.length, 0);
+ const shortcut = await installWrapperIntegration(43, true, wrapper);
+ assert.equal(shortcut.snapshot.options, `~/.lsfg %command% --windowed`);
+ assert.deepEqual(shortcutWrites, [`~/.lsfg %command% --windowed`]);
- const cleaned = await removeWrapperIntegration(42, false, wrapper, undefined, installed.commandTokenAdded);
+ const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.commandTokenAdded);
+ assert.equal(restored.options, "--windowed");
+
+ const cleaned = await removeWrapperIntegration(42, false, wrapper, installed.commandTokenAdded);
assert.equal(cleaned.options, "FOO=bar %command%".replaceAll(" ", " "));
assert.ok(unregisters.includes(42));
assert.ok(unregisters.includes(43));
@@ -143,46 +125,46 @@ test("reads the matching app-details field and installs/removes Steam integratio
}
});
-test("uses shortcut launch options for a host shortcut without changing its Target", async () => {
+test("AppImage EmuDeck and direct Flatpak shortcuts all stay launch-option based", async () => {
const previousWindow = (globalThis as Record<string, unknown>).window;
const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient;
- const originalOptions = 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"';
- let shortcutOptions = originalOptions;
- let shortcutTarget = "env";
- const shortcutWrites: string[] = [];
- const targetWrites: string[] = [];
- const apps = {
- RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) {
- callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions });
- return { unregister() {} };
+ const cases = [
+ {
+ options: 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"',
+ expected: 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"',
},
- SetShortcutLaunchOptions(_appId: number, options: string) {
- shortcutWrites.push(options);
- shortcutOptions = options;
+ {
+ options: "",
+ expected: "~/.lsfg %command%",
},
- SetShortcutExe(_appId: number, executable: string) {
- targetWrites.push(executable);
- shortcutTarget = executable;
+ {
+ options: "run org.example.Game",
+ expected: "~/.lsfg %command% run org.example.Game",
},
- };
+ ];
(globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout };
- (globalThis as Record<string, unknown>).SteamClient = { Apps: apps };
try {
- const installed = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" });
- assert.equal(installed.originalExecutable, undefined);
- assert.equal(installed.snapshot.target, "env");
- assert.equal(installed.snapshot.options, 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"');
- assert.deepEqual(targetWrites, []);
- assert.deepEqual(shortcutWrites, [installed.snapshot.options]);
-
- const secondInstall = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" });
- assert.equal(secondInstall.snapshot.options, installed.snapshot.options);
- assert.deepEqual(shortcutWrites, [installed.snapshot.options]);
-
- const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, { kind: "host" });
- assert.equal(restored.target, "env");
- assert.equal(restored.options, originalOptions);
- assert.deepEqual(targetWrites, []);
+ for (const [index, item] of cases.entries()) {
+ let shortcutOptions = item.options;
+ const shortcutWrites: string[] = [];
+ (globalThis as Record<string, unknown>).SteamClient = {
+ Apps: {
+ RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) {
+ callback({ strShortcutLaunchOptions: shortcutOptions });
+ return { unregister() {} };
+ },
+ SetShortcutLaunchOptions(_appId: number, options: string) {
+ shortcutWrites.push(options);
+ shortcutOptions = options;
+ },
+ },
+ };
+ const installed = await installWrapperIntegration(100 + index, true, wrapper);
+ assert.equal(installed.snapshot.options, item.expected);
+ assert.deepEqual(shortcutWrites, [item.expected]);
+ const restored = await removeWrapperIntegration(100 + index, true, wrapper, installed.commandTokenAdded);
+ assert.equal(restored.options, item.options);
+ }
} finally {
if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window;
else (globalThis as Record<string, unknown>).window = previousWindow;
@@ -191,65 +173,29 @@ test("uses shortcut launch options for a host shortcut without changing its Targ
}
});
-test("fails closed when shortcut Target ownership or setters are unavailable", async () => {
+test("launch option write failure rolls back the original value", async () => {
const previousWindow = (globalThis as Record<string, unknown>).window;
const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient;
+ let appOptions = "FOO=bar %command%";
+ const writes: string[] = [];
(globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout };
(globalThis as Record<string, unknown>).SteamClient = {
Apps: {
RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) {
- callback({ strShortcutExe: _appId === 99 ? "/usr/bin/flatpak" : "garbage", strShortcutLaunchOptions: "" });
+ callback({ strLaunchOptions: appOptions });
return { unregister() {} };
},
+ SetAppLaunchOptions(_appId: number, options: string) {
+ writes.push(options);
+ appOptions = options;
+ if (options.includes(wrapper)) throw new Error("simulated launch option failure");
+ },
},
};
try {
- await assert.rejects(installWrapperIntegration(98, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /supported executable/);
- await assert.rejects(installWrapperIntegration(99, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target API is unavailable/);
- await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target changed externally/);
- } finally {
- if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window;
- else (globalThis as Record<string, unknown>).window = previousWindow;
- if (previousSteamClient === undefined) delete (globalThis as Record<string, unknown>).SteamClient;
- else (globalThis as Record<string, unknown>).SteamClient = previousSteamClient;
- }
-});
-
-test("restores launch options and shortcut Target when a setter fails after changing them", async () => {
- const previousWindow = (globalThis as Record<string, unknown>).window;
- const previousSteamClient = (globalThis as Record<string, unknown>).SteamClient;
- let appOptions = "FOO=bar %command%";
- let shortcutTarget = "/usr/bin/flatpak";
- const appWrites: string[] = [];
- const targetWrites: string[] = [];
- const apps = {
- RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) {
- callback(appId === 42
- ? { strLaunchOptions: appOptions }
- : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" });
- return { unregister() {} };
- },
- SetAppLaunchOptions(_appId: number, options: string) {
- appWrites.push(options);
- appOptions = options;
- if (options.includes(wrapper)) throw new Error("simulated launch-option write failure");
- },
- SetShortcutExe(_appId: number, executable: string) {
- targetWrites.push(executable);
- shortcutTarget = executable;
- if (executable === wrapper) throw new Error("simulated Target write failure");
- },
- };
- (globalThis as Record<string, unknown>).window = { setTimeout, clearTimeout };
- (globalThis as Record<string, unknown>).SteamClient = { Apps: apps };
- try {
- await assert.rejects(installWrapperIntegration(42, false, wrapper), /simulated launch-option write failure/);
+ await assert.rejects(installWrapperIntegration(42, false, wrapper), /simulated launch option failure/);
assert.equal(appOptions, "FOO=bar %command%");
- assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]);
-
- await assert.rejects(installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /simulated Target write failure/);
- assert.equal(shortcutTarget, "/usr/bin/flatpak");
- assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]);
+ assert.deepEqual(writes, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]);
} finally {
if (previousWindow === undefined) delete (globalThis as Record<string, unknown>).window;
else (globalThis as Record<string, unknown>).window = previousWindow;
diff --git a/tests/test_configuration_profiles.py b/tests/test_configuration_profiles.py
new file mode 100644
index 0000000..789842e
--- /dev/null
+++ b/tests/test_configuration_profiles.py
@@ -0,0 +1,80 @@
+import sys
+import tempfile
+import types
+import unittest
+from pathlib import Path
+from unittest.mock import Mock
+
+
+sys.modules.setdefault(
+ "decky",
+ types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()),
+)
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules"))
+
+from lsfg_vk.config_schema import ConfigurationManager
+from lsfg_vk.configuration import ConfigurationService
+
+
+class ConfigurationProfileTests(unittest.TestCase):
+ def setUp(self):
+ self.tempdir = tempfile.TemporaryDirectory()
+ self.home = Path(self.tempdir.name) / "home" / "deck"
+ self.home.mkdir(parents=True)
+ self.runtime = Mock()
+ self.service = ConfigurationService(runtime_service=self.runtime)
+ self.service.user_home = self.home
+ self.service.config_dir = self.home / ".config/lsfg-vk"
+ self.service.config_file_path = self.service.config_dir / "conf.toml"
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def test_selector_only_profile_survives_round_trip(self):
+ content = """version = 2
+
+[global]
+allow_fp16 = true
+
+[[profile]]
+name = "flatpak:org.example.Game"
+pacing_mode = "vsync"
+multiplier = 3
+flow_scale = 0.8
+performance_mode = false
+override_present_mode = true
+preserve_swapchain_image_count = false
+"""
+ parsed = ConfigurationManager.parse_toml_content_multi_profile(content)
+ self.assertIn("flatpak:org.example.Game", parsed["profiles"])
+ self.assertEqual(parsed["profiles"]["flatpak:org.example.Game"]["active_in"], [])
+ rendered = ConfigurationManager.generate_toml_content_multi_profile(parsed)
+ reparsed = ConfigurationManager.parse_toml_content_multi_profile(rendered)
+ self.assertEqual(reparsed["profiles"]["flatpak:org.example.Game"]["multiplier"], 3)
+
+ def test_game_reset_all_preserves_flatpak_profiles(self):
+ self.service.update_game_config("123", "Steam Game", {"multiplier": 2})
+ self.service.update_flatpak_config("org.example.Game", {"multiplier": 3})
+
+ result = self.service.reset_all_game_configs()
+ data = self.service._get_profile_data()
+
+ self.assertTrue(result["success"])
+ self.assertNotIn("Steam Game", data["profiles"])
+ self.assertIn("flatpak:org.example.Game", data["profiles"])
+ self.assertEqual(data["profiles"]["flatpak:org.example.Game"]["multiplier"], 3)
+
+ def test_flatpak_reset_all_preserves_steam_profiles(self):
+ self.service.update_game_config("123", "Steam Game", {"multiplier": 2})
+ self.service.update_flatpak_config("org.example.Game", {"multiplier": 3})
+
+ result = self.service.reset_all_flatpak_configs()
+ data = self.service._get_profile_data()
+
+ self.assertTrue(result["success"])
+ self.assertIn("Steam Game", data["profiles"])
+ self.assertNotIn("flatpak:org.example.Game", data["profiles"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_flatpak_profile_service.py b/tests/test_flatpak_profile_service.py
new file mode 100644
index 0000000..8d58413
--- /dev/null
+++ b/tests/test_flatpak_profile_service.py
@@ -0,0 +1,210 @@
+import hashlib
+import sys
+import tempfile
+import types
+import unittest
+from pathlib import Path
+from unittest.mock import Mock
+
+
+sys.modules.setdefault(
+ "decky",
+ types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()),
+)
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules"))
+
+from lsfg_vk.configuration import ConfigurationService
+from lsfg_vk.flatpak_profile_service import FlatpakProfileService
+
+
+class FakeFlatpakService:
+ def __init__(self, home: Path):
+ self.user_home = home
+ self.config_dir = home / ".config/lsfg-vk"
+ self.config_file_path = self.config_dir / "conf.toml"
+ self.backup_dir = self.config_dir / "flatpak-overrides"
+ self.state = {"version": 2, "plugin_owned_branches": [], "prepared_apps": {}}
+ self.commands = []
+ self.running = ""
+
+ def _read_state(self):
+ return self.state
+
+ def _write_state(self, state):
+ self.state = state
+
+ def _override_path(self, app_id):
+ return self.user_home / ".local/share/flatpak/overrides" / app_id
+
+ def _backup_path(self, app_id):
+ return self.backup_dir / f"{app_id}.ini"
+
+ @staticmethod
+ def _sha256(content):
+ return hashlib.sha256(content).hexdigest()
+
+ def _snapshot_override(self, app_id):
+ path = self._override_path(app_id)
+ if not path.exists():
+ return False, b""
+ return True, path.read_bytes()
+
+ @staticmethod
+ def _write_file(path, content, mode=0o644):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+ path.chmod(mode)
+
+ def prepare_app(self, app_id):
+ apps = self.state["prepared_apps"]
+ if app_id not in apps:
+ existed, original = self._snapshot_override(app_id)
+ if existed:
+ self._write_file(self._backup_path(app_id), original.decode("utf-8"))
+ apps[app_id] = {"override_existed": existed, "managed_sha256": ""}
+ entry = apps[app_id]
+ baseline = ""
+ if entry["override_existed"]:
+ baseline = self._backup_path(app_id).read_text(encoding="utf-8")
+ managed = baseline + "\n[Context]\nfilesystems=/config:ro;/dll:ro;\n[Environment]\nLSFGVK_CONFIG=/config/conf.toml\nLSFGVK_FLATPAK=1\n"
+ self._write_file(self._override_path(app_id), managed)
+ entry["managed_sha256"] = self._sha256(managed.encode())
+ return {"success": True, "owned": True, "prepared": True, "runtime": "org.freedesktop.Platform/x86_64/24.08", "runtime_branch": "24.08"}
+
+ def remove_app_override(self, app_id):
+ entry = self.state["prepared_apps"].get(app_id)
+ if entry is None:
+ return {"success": True, "prepared": False, "owned": False}
+ existed, current = self._snapshot_override(app_id)
+ current_hash = self._sha256(current) if existed else self._sha256(b"")
+ if current_hash != entry["managed_sha256"]:
+ return {"success": False, "error": "Flatpak override changed after preparation"}
+ path = self._override_path(app_id)
+ backup = self._backup_path(app_id)
+ if entry["override_existed"]:
+ self._write_file(path, backup.read_text(encoding="utf-8"))
+ else:
+ path.unlink(missing_ok=True)
+ backup.unlink(missing_ok=True)
+ self.state["prepared_apps"].pop(app_id)
+ return {"success": True, "prepared": False, "owned": False}
+
+ def get_flatpak_apps(self):
+ app_id = "org.example.Game"
+ return {
+ "success": True,
+ "apps": [{
+ "app_id": app_id,
+ "app_name": "Example Game",
+ "runtime": "org.freedesktop.Platform/x86_64/24.08",
+ "runtime_branch": "24.08",
+ "runtime_ready": True,
+ "prepared": app_id in self.state["prepared_apps"],
+ "owned": app_id in self.state["prepared_apps"],
+ "error": None,
+ }],
+ }
+
+ def _run_flatpak_command(self, args, **_kwargs):
+ self.commands.append(args)
+ if args[:3] == ["override", "--user", "--show"]:
+ path = self._override_path(args[3])
+ return types.SimpleNamespace(returncode=0, stdout=path.read_text(encoding="utf-8") if path.exists() else "", stderr="")
+ if args[0] == "override":
+ app_id = args[-1]
+ path = self._override_path(app_id)
+ content = path.read_text(encoding="utf-8") if path.exists() else ""
+ env = [item.removeprefix("--env=") for item in args if item.startswith("--env=")]
+ unset = [item.removeprefix("--unset-env=") for item in args if item.startswith("--unset-env=")]
+ if unset:
+ content += "\n[Context]\nunset-environment=" + ";".join(unset) + ";\n"
+ if env:
+ content += "\n[Environment]\n" + "\n".join(env) + "\n"
+ self._write_file(path, content)
+ return types.SimpleNamespace(returncode=0, stdout="", stderr="")
+ if args[0] == "ps":
+ return types.SimpleNamespace(returncode=0, stdout=self.running, stderr="")
+ raise AssertionError(args)
+
+
+class FlatpakProfileServiceTests(unittest.TestCase):
+ def setUp(self):
+ self.tempdir = tempfile.TemporaryDirectory()
+ self.home = Path(self.tempdir.name) / "home" / "deck"
+ self.home.mkdir(parents=True)
+ self.flatpak = FakeFlatpakService(self.home)
+ self.runtime = Mock()
+ self.configuration = ConfigurationService(runtime_service=self.runtime)
+ self.configuration.user_home = self.home
+ self.configuration.config_dir = self.flatpak.config_dir
+ self.configuration.config_file_path = self.flatpak.config_file_path
+ self.service = FlatpakProfileService(self.flatpak, self.configuration)
+ self.app_id = "org.example.Game"
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def test_enable_creates_selector_profile_and_default_workarounds(self):
+ result = self.service.enable_app(self.app_id)
+ config = self.configuration.get_flatpak_config(self.app_id)
+ content = self.flatpak._override_path(self.app_id).read_text(encoding="utf-8")
+
+ self.assertTrue(result["success"])
+ self.assertTrue(config["exists"])
+ self.assertEqual(config["profile"], "flatpak:org.example.Game")
+ self.assertEqual(config["config"]["active_in"], [])
+ self.assertIn("LSFGVK_PROFILE=flatpak:org.example.Game", content)
+ self.assertIn("ENABLE_GAMESCOPE_WSI=0", content)
+ self.assertIn("DXVK_HDR=0", content)
+
+ def test_workaround_update_rebuilds_from_original_override(self):
+ baseline = "[Environment]\nDXVK_CONFIG=dxgi.syncInterval = 0\nKEEP=yes\n"
+ self.flatpak._write_file(self.flatpak._override_path(self.app_id), baseline)
+ self.assertTrue(self.service.enable_app(self.app_id)["success"])
+
+ state = self.service.default_state()
+ state.update({"dxvkFrameRate": 30, "disableHdr": False, "enableZink": True})
+ result = self.service.set_workaround_state(self.app_id, state)
+ command = next(
+ args for args in reversed(self.flatpak.commands)
+ if args[0] == "override" and any(item.startswith("--env=LSFGVK_PROFILE=") for item in args)
+ )
+
+ self.assertTrue(result["success"])
+ self.assertIn("--env=DXVK_CONFIG=dxgi.syncInterval = 0; dxvk.maxFrameRate = 30", command)
+ self.assertNotIn("--env=DXVK_HDR=0", command)
+ self.assertIn("--env=MESA_LOADER_DRIVER_OVERRIDE=zink", command)
+
+ def test_remove_restores_exact_original_override_and_profile(self):
+ baseline = "[Environment]\nKEEP=yes\n"
+ self.flatpak._write_file(self.flatpak._override_path(self.app_id), baseline)
+ self.assertTrue(self.service.enable_app(self.app_id)["success"])
+
+ removed = self.service.remove_app(self.app_id)
+
+ self.assertTrue(removed["success"])
+ self.assertEqual(self.flatpak._override_path(self.app_id).read_text(encoding="utf-8"), baseline)
+ self.assertFalse(self.configuration.get_flatpak_config(self.app_id)["exists"])
+
+ def test_external_override_change_fails_closed(self):
+ self.assertTrue(self.service.enable_app(self.app_id)["success"])
+ path = self.flatpak._override_path(self.app_id)
+ path.write_text(path.read_text(encoding="utf-8") + "EXTERNAL=yes\n", encoding="utf-8")
+
+ result = self.service.set_workaround_state(self.app_id, self.service.default_state())
+
+ self.assertFalse(result["success"])
+ self.assertIn("changed after preparation", result["error"])
+
+ def test_running_detection_uses_owned_selector_state(self):
+ self.assertTrue(self.service.enable_app(self.app_id)["success"])
+ self.flatpak.running = "org.example.Game\ttrue\t1234\norg.other.App\ttrue\t9999\n"
+
+ result = self.service.get_running_apps()
+
+ self.assertTrue(result["success"])
+ self.assertEqual(result["apps"], [{"app_id": self.app_id, "active": True, "pid": "1234"}])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py
index d5baf61..4d804c7 100644
--- a/tests/test_flatpak_service.py
+++ b/tests/test_flatpak_service.py
@@ -25,16 +25,17 @@ class FlatpakServiceTests(unittest.TestCase):
self.service.user_home = self.home
self.service.config_dir = self.home / ".config/lsfg-vk"
self.service.config_file_path = self.service.config_dir / "conf.toml"
+ self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True)
self.service.check_flatpak_available = Mock(return_value=True)
self.service._run_flatpak_command = Mock(side_effect=self._run_flatpak_command)
self.runtime_ref = "org.freedesktop.Platform/x86_64/24.08"
self.runtime_metadata = ""
self.user_branches = set()
self.system_branches = set()
- self.install_branch = "24.08"
- self.bundle = self.home / "lsfg-vk-24.08.flatpak"
- self.bundle.write_bytes(b"bundle")
- self.service._bundled_extension_path = Mock(return_value=self.bundle)
+ self.user_extension_origin = "flathub"
+ self.apps = {"com.example.Game": "Example Game"}
+ self.dll_dir = self.home / ".local/share/Steam/steamapps/common/Lossless Scaling"
+ self.service._dll_directory = Mock(return_value=self.dll_dir)
def tearDown(self):
self.tempdir.cleanup()
@@ -47,252 +48,266 @@ class FlatpakServiceTests(unittest.TestCase):
def _extension_line(branch):
return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n"
+ @staticmethod
+ def _parse_override(content):
+ section = None
+ filesystems = []
+ unset_environment = []
+ environment = {}
+ other = []
+ for raw in content.splitlines():
+ line = raw.strip()
+ if line.startswith("[") and line.endswith("]"):
+ section = line[1:-1]
+ continue
+ key, separator, value = line.partition("=")
+ if not separator:
+ continue
+ if section == "Context" and key == "filesystems":
+ filesystems.extend(item for item in value.split(";") if item)
+ elif section == "Context" and key == "unset-environment":
+ unset_environment.extend(item for item in value.split(";") if item)
+ elif section == "Environment":
+ environment[key] = value
+ else:
+ other.append((section, key, value))
+ return filesystems, unset_environment, environment, other
+
+ @staticmethod
+ def _serialize_override(filesystems, unset_environment, environment):
+ lines = ["[Context]"]
+ if filesystems:
+ lines.append("filesystems=" + ";".join(filesystems) + ";")
+ if unset_environment:
+ lines.append("unset-environment=" + ";".join(unset_environment) + ";")
+ if environment:
+ lines.append("")
+ lines.append("[Environment]")
+ lines.extend(f"{key}={value}" for key, value in environment.items())
+ return "\n".join(lines) + "\n"
+
+ def _apply_override(self, args):
+ app_id = args[-1]
+ path = self.service._override_path(app_id)
+ content = path.read_text(encoding="utf-8") if path.exists() else ""
+ filesystems, unset_environment, environment, _ = self._parse_override(content)
+ for arg in args[2:-1]:
+ if arg.startswith("--filesystem="):
+ value = arg.split("=", 1)[1]
+ if value not in filesystems:
+ filesystems.append(value)
+ elif arg.startswith("--env="):
+ key, value = arg.split("=", 1)[1].split("=", 1)
+ environment[key] = value
+ if key in unset_environment:
+ unset_environment.remove(key)
+ elif arg.startswith("--unset-env="):
+ key = arg.split("=", 1)[1]
+ environment.pop(key, None)
+ if key not in unset_environment:
+ unset_environment.append(key)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(
+ self._serialize_override(filesystems, unset_environment, environment),
+ encoding="utf-8",
+ )
+ return self._result()
+
def _run_flatpak_command(self, args, **_kwargs):
- if args[0] == "info" and args[1] == "--show-runtime":
+ if args[:2] == ["info", "--show-runtime"]:
return self._result(self.runtime_ref + "\n")
- if args[0] == "info" and args[1] == "--show-metadata":
+ if args[:2] == ["info", "--show-metadata"]:
return self._result(self.runtime_metadata)
+ if args[:3] == ["info", "--user", "--show-origin"]:
+ return self._result(self.user_extension_origin)
+ if args[:2] == ["list", "--app"]:
+ return self._result("".join(f"{name}\t{app_id}\n" for app_id, name in self.apps.items()))
if args[0] == "list":
branches = self.user_branches if "--user" in args else self.system_branches
return self._result("".join(self._extension_line(branch) for branch in sorted(branches)))
if args[0] == "install":
- self.user_branches.add(self.install_branch)
+ self.user_branches.add(self.runtime_ref.rsplit("/", 1)[-1])
return self._result()
if args[0] == "uninstall":
self.user_branches.discard(args[-1].rsplit("/", 1)[-1])
return self._result()
+ if args[:3] == ["override", "--user", "--show"]:
+ path = self.service._override_path(args[-1])
+ return self._result(path.read_text(encoding="utf-8") if path.exists() else "")
+ if args[:2] == ["override", "--user"]:
+ return self._apply_override(args)
raise AssertionError(f"Unexpected Flatpak command: {args}")
- def test_runtime_branch_mapping_is_strict_and_branch_specific(self):
- self.assertEqual(
- FlatpakService.runtime_branch_from_ref(
- "org.freedesktop.Platform/x86_64/24.08"
- ),
- "24.08",
- )
- self.assertEqual(
- FlatpakService.runtime_branch_from_ref(
- "org.freedesktop.Platform//25.08"
- ),
- "25.08",
- )
- with self.assertRaises(ValueError):
- FlatpakService.runtime_branch_from_ref("org.gnome.Sdk/x86_64/46")
- with self.assertRaises(ValueError):
- FlatpakService.runtime_branch_from_ref(
- "org.freedesktop.Platform/x86_64/26.08"
- )
-
- def test_runtime_branch_mapping_reads_documented_gl_metadata(self):
- metadata = """
-[Extension org.freedesktop.Platform.GL]
-versions=25.08;25.08-extra;1.4
-version=1.4
-"""
- self.assertEqual(FlatpakService.runtime_branch_from_metadata(metadata), "25.08")
- with self.assertRaises(ValueError):
- FlatpakService.runtime_branch_from_metadata(
- "[Extension org.freedesktop.Platform.GL]\nversions=26.08;26.08-extra;1.4\n"
- )
-
- def test_resolve_reads_required_runtime_instead_of_any_installed_branch(self):
- self.user_branches = {"23.08"}
-
- response = self.service.resolve_app_support("com.example.Game")
+ def test_resolves_freedesktop_and_derived_runtimes(self):
+ runtime, branch = self.service._resolve_runtime("com.example.Game")
+ self.assertEqual(runtime, self.runtime_ref)
+ self.assertEqual(branch, "24.08")
+
+ self.runtime_ref = "org.kde.Platform/x86_64/6.10"
+ self.runtime_metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n"
+ runtime, branch = self.service._resolve_runtime("com.example.Game")
+ self.assertEqual(runtime, self.runtime_ref)
+ self.assertEqual(branch, "25.08")
+
+ def test_prepare_app_installs_runtime_and_persists_narrow_override(self):
+ response = self.service.prepare_app("com.example.Game")
self.assertTrue(response["success"])
+ self.assertTrue(response["prepared"])
+ self.assertTrue(response["owned"])
self.assertEqual(response["runtime_branch"], "24.08")
- self.assertEqual(response["support_status"], "needs-runtime")
- self.assertFalse(response["extension_installed"])
- self.assertEqual(
- self.service._run_flatpak_command.call_args_list[0].args[0],
- ["info", "--show-runtime", "com.example.Game"],
- )
- self.assertEqual(
- self.service._run_flatpak_command.call_args_list[1].args[0],
- ["list", "--user", "--runtime", "--columns=application,arch,branch"],
- )
+ self.assertEqual(self.user_branches, {"24.08"})
+ install_calls = [
+ call.args[0]
+ for call in self.service._run_flatpak_command.call_args_list
+ if call.args[0][0] == "install"
+ ]
self.assertEqual(
- self.service._run_flatpak_command.call_args_list[2].args[0],
- ["list", "--system", "--runtime", "--columns=application,arch,branch"],
+ install_calls,
+ [[
+ "install",
+ "--user",
+ "--noninteractive",
+ "--or-update",
+ "flathub",
+ "org.freedesktop.Platform.VulkanLayer.lsfgvk//24.08",
+ ]],
)
+ status = self.service._app_override_status("com.example.Game")
+ self.assertTrue(status["prepared"])
+ content = self.service._override_path("com.example.Game").read_text(encoding="utf-8")
+ self.assertIn(str(self.service.config_dir) + ":ro", content)
+ self.assertIn(str(self.dll_dir) + ":ro", content)
+ self.assertIn("LSFGVK_CONFIG=" + str(self.service.config_file_path), content)
+ self.assertIn("LSFGVK_FLATPAK=1", content)
+ self.assertNotIn("ENABLE_GAMESCOPE_WSI", content)
+ state = json.loads(self.service.ownership_path.read_text(encoding="utf-8"))
+ self.assertEqual(state["plugin_owned_branches"], ["24.08"])
+ self.assertIn("com.example.Game", state["prepared_apps"])
+
+ def test_prepare_is_idempotent(self):
+ first = self.service.prepare_app("com.example.Game")
+ first_content = self.service._override_path("com.example.Game").read_bytes()
+ second = self.service.prepare_app("com.example.Game")
+
+ self.assertTrue(first["success"])
+ self.assertTrue(second["success"])
+ self.assertEqual(first_content, self.service._override_path("com.example.Game").read_bytes())
+ install_calls = [call for call in self.service._run_flatpak_command.call_args_list if call.args[0][0] == "install"]
+ self.assertEqual(len(install_calls), 1)
+
+ def test_replaces_extension_from_another_remote(self):
+ self.user_branches = {"24.08"}
+ self.user_extension_origin = "lsfgvk-origin"
- def test_resolve_maps_kde_and_gnome_runtimes_from_gl_metadata(self):
- metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n"
- for runtime in ("org.kde.Platform/x86_64/6.10", "org.gnome.Platform/x86_64/49"):
- with self.subTest(runtime=runtime):
- self.service._run_flatpak_command.reset_mock()
- self.runtime_ref = runtime
- self.runtime_metadata = metadata
- response = self.service.resolve_app_support("com.example.Game")
- self.assertEqual(response["runtime_branch"], "25.08")
- self.assertEqual(response["support_status"], "needs-runtime")
- self.assertEqual(
- self.service._run_flatpak_command.call_args_list[1].args[0],
- ["info", "--show-metadata", runtime],
- )
-
- def test_system_extension_is_ready_without_installing_a_user_copy(self):
- self.system_branches = {"24.08"}
-
- response = self.service.ensure_app_support("com.example.Game")
+ response = self.service.install_extension("24.08")
self.assertTrue(response["success"])
- self.assertEqual(response["support_status"], "ready")
- self.assertEqual(
- [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list],
- ["info", "list", "list"],
+ commands = [call.args[0] for call in self.service._run_flatpak_command.call_args_list]
+ self.assertIn(
+ [
+ "uninstall",
+ "--user",
+ "--noninteractive",
+ "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/24.08",
+ ],
+ commands,
)
- self.assertFalse(any(call.args[0][0] == "install" for call in self.service._run_flatpak_command.call_args_list))
+ self.assertEqual(self.user_branches, {"24.08"})
- def test_install_records_only_a_new_user_owned_branch(self):
- response = self.service.install_extension("24.08")
+ def test_preinstalled_runtime_is_not_owned(self):
+ self.system_branches = {"24.08"}
+ response = self.service.prepare_app("com.example.Game")
self.assertTrue(response["success"])
- self.assertTrue(response["enabled"])
- self.assertTrue(response["installed"])
- install_args = self.service._run_flatpak_command.call_args_list[2].args[0]
- self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"])
- self.assertEqual(
- json.loads(self.service.ownership_path.read_text(encoding="utf-8")),
- {"version": 1, "plugin_owned_branches": ["24.08"]},
+ state = json.loads(self.service.ownership_path.read_text(encoding="utf-8"))
+ self.assertEqual(state["plugin_owned_branches"], [])
+ self.assertIn("com.example.Game", state["prepared_apps"])
+
+ def test_external_preparation_is_preserved(self):
+ path = self.service._override_path("com.example.Game")
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(
+ self._serialize_override(
+ [str(self.service.config_dir) + ":ro", str(self.dll_dir) + ":ro"],
+ ["DISABLE_LSFGVK", "DISABLE_LSFG"],
+ {
+ "LSFGVK_CONFIG": str(self.service.config_file_path),
+ "LSFGVK_FLATPAK": "1",
+ },
+ ),
+ encoding="utf-8",
)
+ self.system_branches = {"24.08"}
- def test_preexisting_branch_is_not_claimed_or_removed(self):
- self.user_branches = {"24.08"}
-
- install_response = self.service.install_extension("24.08")
- cleanup_response = self.service.remove_plugin_owned_extensions()
+ response = self.service.prepare_app("com.example.Game")
- self.assertTrue(install_response["success"])
- self.assertTrue(install_response["enabled"])
- self.assertTrue(install_response["installed"])
+ self.assertTrue(response["success"])
+ self.assertTrue(response["prepared"])
+ self.assertFalse(response["owned"])
self.assertFalse(self.service.ownership_path.exists())
- self.assertTrue(cleanup_response["success"])
- self.assertEqual(self.service._run_flatpak_command.call_count, 2)
- def test_extension_toggle_preserves_preexisting_branch(self):
- self.user_branches = {"24.08"}
+ def test_remove_restores_exact_previous_override(self):
+ self.system_branches = {"24.08"}
+ original = "[Context]\nfilesystems=~/Documents;\n\n[Environment]\nFOO=bar\n"
+ path = self.service._override_path("com.example.Game")
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(original, encoding="utf-8")
+ self.assertTrue(self.service.prepare_app("com.example.Game")["success"])
- enable_response = self.service.set_extension_enabled("24.08", True)
- disable_response = self.service.set_extension_enabled("24.08", False)
+ response = self.service.remove_app_override("com.example.Game")
- self.assertTrue(enable_response["success"])
- self.assertTrue(enable_response["enabled"])
- self.assertTrue(enable_response["installed"])
- self.assertTrue(disable_response["success"])
- self.assertTrue(disable_response["enabled"])
- self.assertTrue(disable_response["installed"])
- self.assertFalse(disable_response["removed"])
- self.assertEqual(self.user_branches, {"24.08"})
- self.assertEqual(
- [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list],
- ["list", "list", "list", "list"],
- )
+ self.assertTrue(response["success"])
+ self.assertEqual(path.read_text(encoding="utf-8"), original)
+ self.assertFalse(self.service.ownership_path.exists())
- def test_extension_toggle_removes_owned_user_branch_but_preserves_system_branch(self):
- self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True)
- self.service.ownership_path.write_text(
- json.dumps({"version": 1, "plugin_owned_branches": ["24.08"]}),
- encoding="utf-8",
- )
- self.user_branches = {"24.08"}
+ def test_remove_deletes_override_created_by_plugin(self):
self.system_branches = {"24.08"}
+ self.assertTrue(self.service.prepare_app("com.example.Game")["success"])
+ path = self.service._override_path("com.example.Game")
+ self.assertTrue(path.exists())
- disable_response = self.service.set_extension_enabled("24.08", False)
- repeat_response = self.service.set_extension_enabled("24.08", False)
+ response = self.service.remove_app_override("com.example.Game")
- self.assertTrue(disable_response["success"])
- self.assertTrue(disable_response["enabled"])
- self.assertTrue(disable_response["removed"])
- self.assertTrue(repeat_response["success"])
- self.assertTrue(repeat_response["enabled"])
- self.assertTrue(repeat_response["installed"])
- self.assertEqual(self.user_branches, set())
- self.assertEqual(self.system_branches, {"24.08"})
+ self.assertTrue(response["success"])
+ self.assertFalse(path.exists())
self.assertFalse(self.service.ownership_path.exists())
- uninstall_commands = [
- call.args[0]
- for call in self.service._run_flatpak_command.call_args_list
- if call.args[0][0] == "uninstall"
- ]
- self.assertEqual(len(uninstall_commands), 1)
- def test_corrupt_ownership_metadata_fails_closed(self):
- self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True)
- self.service.ownership_path.write_text("{not-json", encoding="utf-8")
+ def test_remove_fails_closed_after_external_change(self):
+ self.system_branches = {"24.08"}
+ self.assertTrue(self.service.prepare_app("com.example.Game")["success"])
+ path = self.service._override_path("com.example.Game")
+ with path.open("a", encoding="utf-8") as handle:
+ handle.write("EXTERNAL=1\n")
- response = self.service.remove_plugin_owned_extensions()
+ response = self.service.remove_app_override("com.example.Game")
self.assertFalse(response["success"])
- self.assertTrue(response["ownership_uncertain"])
- self.assertEqual(self.service._run_flatpak_command.call_count, 0)
+ self.assertIn("changed after preparation", response["error"])
+ self.assertTrue(path.exists())
+ self.assertTrue(self.service.ownership_path.exists())
- def test_dangling_ownership_symlink_fails_closed(self):
- self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True)
- self.service.ownership_path.symlink_to(self.home / "missing-metadata")
-
- response = self.service.remove_plugin_owned_extensions()
-
- self.assertFalse(response["success"])
- self.assertTrue(response["ownership_uncertain"])
- self.assertEqual(self.service._run_flatpak_command.call_count, 0)
+ def test_full_cleanup_removes_only_owned_state(self):
+ self.system_branches = {"23.08"}
+ self.assertTrue(self.service.prepare_app("com.example.Game")["success"])
+ self.assertEqual(self.user_branches, {"24.08"})
- def test_ensure_app_support_installs_only_the_app_runtime_branch(self):
- response = self.service.ensure_app_support("com.example.Game")
+ response = self.service.remove_plugin_owned_environment()
self.assertTrue(response["success"])
- self.assertEqual(response["support_status"], "ready")
- self.assertEqual(response["runtime_branch"], "24.08")
- install_args = next(
- call.args[0]
- for call in self.service._run_flatpak_command.call_args_list
- if call.args[0][0] == "install"
- )
- self.assertEqual(install_args[0], "install")
- self.assertIn("--user", install_args)
- self.assertNotIn("23.08", install_args)
- self.assertEqual(
- json.loads(self.service.ownership_path.read_text(encoding="utf-8")),
- {"version": 1, "plugin_owned_branches": ["24.08"]},
- )
-
- def test_two_shortcuts_using_one_flatpak_share_one_extension_branch(self):
- first = self.service.ensure_app_support("net.pcsx2.PCSX2")
- second = self.service.ensure_app_support("net.pcsx2.PCSX2.Dev")
-
- self.assertEqual(first["support_status"], "ready")
- self.assertEqual(second["support_status"], "ready")
- install_commands = [
- call.args[0]
- for call in self.service._run_flatpak_command.call_args_list
- if call.args[0][0] == "install"
- ]
- self.assertEqual(len(install_commands), 1)
- self.assertEqual(
- json.loads(self.service.ownership_path.read_text(encoding="utf-8")),
- {"version": 1, "plugin_owned_branches": ["24.08"]},
- )
+ self.assertEqual(response["removed_apps"], ["com.example.Game"])
+ self.assertEqual(response["removed_branches"], ["24.08"])
+ self.assertEqual(self.user_branches, set())
+ self.assertEqual(self.system_branches, {"23.08"})
+ self.assertFalse(self.service.ownership_path.exists())
- def test_cleanup_removes_all_owned_branches_without_reusing_stale_metadata(self):
- self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True)
- self.service.ownership_path.write_text(
- json.dumps({"version": 1, "plugin_owned_branches": ["23.08", "24.08"]}),
- encoding="utf-8",
- )
- self.user_branches = {"23.08", "24.08"}
+ def test_corrupt_ownership_metadata_fails_closed(self):
+ self.service.ownership_path.write_text("{not-json", encoding="utf-8")
- response = self.service.remove_plugin_owned_extensions()
+ response = self.service.remove_plugin_owned_environment()
- self.assertTrue(response["success"])
- self.assertEqual(response["removed_branches"], ["23.08", "24.08"])
- self.assertFalse(self.service.ownership_path.exists())
- uninstall_commands = [
- call.args[0]
- for call in self.service._run_flatpak_command.call_args_list
- if call.args[0][0] == "uninstall"
- ]
- self.assertEqual(len(uninstall_commands), 2)
+ self.assertFalse(response["success"])
+ self.assertEqual(self.service._run_flatpak_command.call_count, 0)
if __name__ == "__main__":
diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py
index a392f85..7ab4621 100644
--- a/tests/test_plugin_migration.py
+++ b/tests/test_plugin_migration.py
@@ -6,7 +6,7 @@ from unittest.mock import Mock
class PluginMigrationTests(unittest.TestCase):
- def test_migration_only_runs_decky_path_migrations(self):
+ def _load_plugin(self):
decky = types.SimpleNamespace(
DECKY_HOME="/decky",
DECKY_USER_HOME="/home/deck",
@@ -17,12 +17,31 @@ class PluginMigrationTests(unittest.TestCase):
)
previous_decky = sys.modules.get("decky")
previous_tomllib = sys.modules.get("tomllib")
+ previous_plugin = sys.modules.pop("lsfg_vk.plugin", None)
sys.modules["decky"] = decky
sys.modules["tomllib"] = types.SimpleNamespace(loads=Mock())
- try:
- sys.path.insert(0, "py_modules")
- from lsfg_vk.plugin import Plugin
+ sys.path.insert(0, "py_modules")
+ from lsfg_vk.plugin import Plugin
+ return Plugin, decky, previous_decky, previous_tomllib, previous_plugin
+
+ def _restore(self, previous_decky, previous_tomllib, previous_plugin):
+ sys.path.remove("py_modules")
+ if previous_decky is None:
+ sys.modules.pop("decky", None)
+ else:
+ sys.modules["decky"] = previous_decky
+ if previous_tomllib is None:
+ sys.modules.pop("tomllib", None)
+ else:
+ sys.modules["tomllib"] = previous_tomllib
+ if previous_plugin is None:
+ sys.modules.pop("lsfg_vk.plugin", None)
+ else:
+ sys.modules["lsfg_vk.plugin"] = previous_plugin
+ def test_migration_only_runs_decky_path_migrations(self):
+ Plugin, decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin()
+ try:
plugin = Plugin.__new__(Plugin)
plugin.installation_service = Mock()
plugin.flatpak_service = Mock()
@@ -33,17 +52,42 @@ class PluginMigrationTests(unittest.TestCase):
decky.migrate_settings.assert_called_once()
decky.migrate_runtime.assert_called_once()
plugin.installation_service.install.assert_not_called()
- plugin.flatpak_service.migrate_v2.assert_not_called()
+ plugin.flatpak_service.prepare_app.assert_not_called()
+ finally:
+ self._restore(previous_decky, previous_tomllib, previous_plugin)
+
+ def test_uninstall_cleans_owned_flatpak_state_and_profiles(self):
+ Plugin, _decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin()
+ try:
+ plugin = Plugin.__new__(Plugin)
+ plugin.installation_service = Mock()
+ plugin.flatpak_service = Mock()
+ plugin.configuration_service = Mock()
+ plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True}
+
+ asyncio.run(plugin._uninstall())
+
+ plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with()
+ plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with()
+ plugin.installation_service.cleanup_on_uninstall.assert_called_once_with()
+ finally:
+ self._restore(previous_decky, previous_tomllib, previous_plugin)
+
+ def test_uninstall_preserves_profiles_when_flatpak_cleanup_fails(self):
+ Plugin, _decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin()
+ try:
+ plugin = Plugin.__new__(Plugin)
+ plugin.installation_service = Mock()
+ plugin.flatpak_service = Mock()
+ plugin.configuration_service = Mock()
+ plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": False, "error": "changed"}
+
+ asyncio.run(plugin._uninstall())
+
+ plugin.configuration_service.reset_all_flatpak_configs.assert_not_called()
+ plugin.installation_service.cleanup_on_uninstall.assert_called_once_with()
finally:
- sys.path.remove("py_modules")
- if previous_decky is None:
- sys.modules.pop("decky", None)
- else:
- sys.modules["decky"] = previous_decky
- if previous_tomllib is None:
- sys.modules.pop("tomllib", None)
- else:
- sys.modules["tomllib"] = previous_tomllib
+ self._restore(previous_decky, previous_tomllib, previous_plugin)
if __name__ == "__main__":
diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py
index 636c87f..004ffb6 100644
--- a/tests/test_steam_service.py
+++ b/tests/test_steam_service.py
@@ -11,73 +11,11 @@ sys.modules.setdefault(
)
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules"))
-from lsfg_vk.steam_service import SteamService, classify_shortcut_transport
+from lsfg_vk.steam_service import SteamService
-class SteamTransportTests(unittest.TestCase):
- def test_only_direct_canonical_flatpak_forms_are_classified(self):
- self.assertEqual(
- classify_shortcut_transport(
- "/usr/bin/flatpak",
- "run com.example.PCSX2 --fullscreen",
- ),
- {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "flatpak",
- "run com.example.PCSX2 --fullscreen",
- ),
- {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "/usr/bin/flatpak run com.example.PCSX2",
- "--fullscreen",
- ),
- {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "/usr/bin/bash",
- "~/launch-game.sh --fullscreen",
- ),
- {"kind": "host"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "/usr/bin/flatpak",
- "--user run com.example.PCSX2",
- ),
- {"kind": "host"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "/usr/bin/flatpak",
- "run bash ~/launch-game.sh",
- ),
- {"kind": "host"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "~/.lsfg",
- "run --branch=stable --arch=x86_64 com.example.PCSX2",
- ),
- {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"},
- )
- self.assertEqual(
- classify_shortcut_transport(
- "/home/deck/.lsfg",
- "run com.example.PCSX2",
- ),
- {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"},
- )
- self.assertEqual(
- classify_shortcut_transport("~/.lsfg", "--profile high"),
- {"kind": "host"},
- )
-
- def test_shortcut_data_preserves_transport_inputs(self):
+class SteamShortcutTests(unittest.TestCase):
+ def test_direct_flatpak_shortcut_is_ordinary_non_steam_metadata(self):
game = SteamService._shortcut_game(
{
"appid": 123456,
@@ -88,30 +26,33 @@ class SteamTransportTests(unittest.TestCase):
}
)
- self.assertEqual(game["appid"], "123456")
- self.assertEqual(game["transport"], {
- "kind": "flatpak",
- "flatpakAppId": "net.pcsx2.PCSX2",
+ self.assertEqual(game, {
+ "appid": "123456",
+ "name": "PCSX2 shortcut",
+ "nonSteam": True,
})
- self.assertEqual(game["executable"], "/usr/bin/flatpak")
- self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen")
- self.assertEqual(game["startDir"], "/home/deck/Games")
- def test_wrapped_flatpak_shortcut_remains_a_flatpak_target(self):
+ def test_emudeck_launcher_is_ordinary_non_steam_metadata(self):
game = SteamService._shortcut_game(
{
"appid": 987654,
- "AppName": "Wrapped Flatpak",
- "Exe": "~/.lsfg",
- "LaunchOptions": "run --branch=stable --arch=x86_64 com.example.Game",
+ "AppName": "1080 Snowboarding",
+ "Exe": '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64',
+ "LaunchOptions": "",
}
)
- self.assertEqual(game["transport"], {
- "kind": "flatpak",
- "flatpakAppId": "com.example.Game",
+ self.assertEqual(game, {
+ "appid": "987654",
+ "name": "1080 Snowboarding",
+ "nonSteam": True,
})
+ def test_shortcut_rejects_invalid_identity(self):
+ self.assertIsNone(SteamService._shortcut_game({"appid": 0, "AppName": "Bad"}))
+ self.assertIsNone(SteamService._shortcut_game({"appid": 1, "AppName": ""}))
+ self.assertIsNone(SteamService._shortcut_game("bad"))
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py
index 0632d93..5c291c7 100644
--- a/tests/test_wrapper_service.py
+++ b/tests/test_wrapper_service.py
@@ -1,4 +1,3 @@
-import os
import subprocess
import sys
import tempfile
@@ -24,10 +23,10 @@ class WrapperServiceTests(unittest.TestCase):
self.home.mkdir(parents=True)
self.service = WrapperService()
self.service.user_home = self.home
- self.service.local_bin_dir = self.home / ".local/bin"
self.service.config_dir = self.home / ".config/lsfg-vk"
+ self.service.config_file_path = self.service.config_dir / "conf.toml"
self.service.sidecar_path = self.service.config_dir / "workarounds.json"
- self.service.wrapper_path = self.service.local_bin_dir / "lsfg"
+ self.service.wrapper_path = self.home / ".lsfg"
def tearDown(self):
self.tempdir.cleanup()
@@ -59,7 +58,7 @@ class WrapperServiceTests(unittest.TestCase):
self.assertIn(self.service.MARKER, self.service.wrapper_path.read_text(encoding="utf-8"))
self.assertEqual(self.service.get("123")["state"], self._state(dxvkFrameRate=60, enableZink=True))
- def test_dispatch_clears_managed_values_preserves_other_environment_and_appends_config(self):
+ def test_dispatch_exports_appid_config_and_workarounds(self):
self.service.set(
"123",
self._state(dxvkFrameRate=30, disableSteamdeckMode=True, disableVkbasalt=True, enableZink=True),
@@ -71,12 +70,16 @@ class WrapperServiceTests(unittest.TestCase):
"DXVK_CONFIG": "dxgi.syncInterval = 0",
"DXVK_FRAME_RATE": "5",
"ENABLE_GAMESCOPE_WSI": "1",
+ "DISABLE_LSFGVK": "1",
+ "DISABLE_LSFG": "1",
"DISABLE_VKBASALT": "0",
"MESA_LOADER_DRIVER_OVERRIDE": "llvmpipe",
"MANGOHUD": "1",
},
)
values = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)
+ self.assertEqual(values["SteamAppId"], "123")
+ self.assertEqual(values["LSFGVK_CONFIG"], str(self.service.config_file_path))
self.assertEqual(values["ENABLE_GAMESCOPE_WSI"], "0")
self.assertEqual(values["DXVK_HDR"], "0")
self.assertEqual(values["SteamDeck"], "0")
@@ -88,6 +91,20 @@ class WrapperServiceTests(unittest.TestCase):
self.assertEqual(values["MANGOHUD"], "1")
self.assertNotIn("DXVK_FRAME_RATE", values)
self.assertNotIn("ENABLE_VKBASALT", values)
+ self.assertNotIn("DISABLE_LSFGVK", values)
+ self.assertNotIn("DISABLE_LSFG", values)
+
+ def test_wrapper_is_transport_agnostic(self):
+ self.service.set("123", self._state())
+ fake = self.home / "target"
+ fake.write_text("#!/bin/sh\nprintf '%s\\n' \"$@\"\n", encoding="utf-8")
+ fake.chmod(0o755)
+ result = self._run(123, str(fake), "run", "org.example.Game")
+ self.assertEqual(result.stdout.splitlines(), ["run", "org.example.Game"])
+ content = self.service.wrapper_path.read_text(encoding="utf-8")
+ self.assertNotIn("flatpakAppId", content)
+ self.assertNotIn("shortcut_exe", content)
+ self.assertNotIn("--filesystem", content)
def test_appid_fallback_and_unmatched_passthrough(self):
self.service.set("123", self._state(disableGamescopeWsi=False, disableHdr=False))
@@ -101,7 +118,7 @@ class WrapperServiceTests(unittest.TestCase):
)
fallback_values = dict(line.split("=", 1) for line in fallback.stdout.splitlines() if "=" in line)
self.assertEqual(fallback_values["SteamDeck"], "0")
- self.assertEqual(fallback_values["SteamGameId"], "456")
+ self.assertEqual(fallback_values["SteamAppId"], "456")
passthrough = subprocess.run(
[str(self.service.wrapper_path), "/usr/bin/env"],
@@ -114,139 +131,19 @@ class WrapperServiceTests(unittest.TestCase):
self.assertEqual(passthrough_values["KEEP"], "yes")
self.assertEqual(passthrough_values["DXVK_HDR"], "1")
- def test_flatpak_shortcut_receives_env_arguments_and_original_target(self):
- fake_flatpak = self.home / ".local/bin/flatpak"
- fake_flatpak.parent.mkdir(parents=True, exist_ok=True)
- fake_flatpak.write_text(
- "#!/bin/sh\n"
- "printf 'ARG:%s\\n' \"$@\"\n",
- encoding="utf-8",
- )
- fake_flatpak.chmod(0o755)
- self.service.set(
- "123",
- self._state(dxvkFrameRate=20, enableZink=True),
- str(fake_flatpak),
- False,
- {"kind": "flatpak", "flatpakAppId": "com.example.Game"},
- )
- result = self._run(123, "run", "com.example.Game", "--windowed", env={"DXVK_CONFIG": "foo=1"})
- args = result.stdout.splitlines()
- self.assertEqual(args[0], "ARG:run")
- self.assertIn("ARG:--filesystem=" + str(self.service.config_dir) + ":rw", args)
- self.assertIn("ARG:--filesystem=" + str(self.home / ".local/share/Steam/steamapps/common/Lossless Scaling") + ":ro", args)
- self.assertIn("ARG:--env=LSFGVK_CONFIG=" + str(self.service.config_file_path), args)
- self.assertIn("ARG:--env=LSFGVK_FLATPAK=1", args)
- self.assertIn("ARG:--env=SteamAppId=123", args)
- self.assertIn("ARG:--env=ENABLE_GAMESCOPE_WSI=0", args)
- self.assertIn("ARG:--env=DXVK_HDR=0", args)
- self.assertIn("ARG:--env=__GLX_VENDOR_LIBRARY_NAME=mesa", args)
- self.assertIn("ARG:--env=MESA_LOADER_DRIVER_OVERRIDE=zink", args)
- self.assertIn("ARG:--env=GALLIUM_DRIVER=zink", args)
- self.assertIn("ARG:--env=DXVK_CONFIG=foo=1; dxvk.maxFrameRate = 20", args)
- self.assertIn("ARG:com.example.Game", args)
- self.assertIn("ARG:--windowed", args)
-
- def test_flatpak_full_executable_form_is_preserved(self):
- fake_flatpak = self.home / ".local/bin/flatpak"
- fake_flatpak.parent.mkdir(parents=True, exist_ok=True)
- fake_flatpak.write_text(
- "#!/bin/sh\n"
- "printf 'ARG:%s\\n' \"$@\"\n",
- encoding="utf-8",
- )
- fake_flatpak.chmod(0o755)
- response = self.service.set(
- "123",
- self._state(),
- f"{fake_flatpak} run com.example.Game",
- False,
- {"kind": "flatpak", "flatpakAppId": "com.example.Game"},
- )
- self.assertTrue(response["success"])
- result = self._run(123, "--windowed")
- args = result.stdout.splitlines()
- self.assertEqual(args[0], "ARG:run")
- self.assertIn("ARG:com.example.Game", args)
- self.assertIn("ARG:--windowed", args)
-
- def test_host_transport_does_not_store_shortcut_target(self):
- self.service.set(
- "123",
- self._state(),
- "/usr/bin/flatpak",
- False,
- {"kind": "flatpak", "flatpakAppId": "com.example.Game"},
- )
- response = self.service.set(
- "123",
- self._state(),
- "/usr/bin/ignored",
- False,
- {"kind": "host"},
- )
- self.assertTrue(response["success"])
- self.assertIsNone(response["shortcut_exe"])
- self.assertIsNone(self.service.get("123")["shortcut_exe"])
-
- def test_flatpak_transport_rejects_non_run_invocation(self):
- fake_flatpak = self.home / ".local/bin/flatpak"
- fake_flatpak.parent.mkdir(parents=True, exist_ok=True)
- fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
- fake_flatpak.chmod(0o755)
- response = self.service.set(
- "123",
- self._state(),
- str(fake_flatpak),
- False,
- {"kind": "flatpak", "flatpakAppId": "com.example.Game"},
- )
- self.assertTrue(response["success"])
- result = subprocess.run(
- [str(self.service.wrapper_path), "bash", "launch-game.sh"],
- env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"},
- capture_output=True,
- text=True,
- )
- self.assertEqual(result.returncode, 64)
- self.assertIn("direct flatpak run", result.stderr)
-
- def test_flatpak_transport_rejects_external_app_id_change(self):
- fake_flatpak = self.home / ".local/bin/flatpak"
- fake_flatpak.parent.mkdir(parents=True, exist_ok=True)
- fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
- fake_flatpak.chmod(0o755)
- response = self.service.set(
- "123",
- self._state(),
- str(fake_flatpak),
- False,
- {"kind": "flatpak", "flatpakAppId": "com.example.Game"},
- )
- self.assertTrue(response["success"])
- result = subprocess.run(
- [str(self.service.wrapper_path), "run", "com.other.Game"],
- env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"},
- capture_output=True,
- text=True,
- )
- self.assertEqual(result.returncode, 64)
- self.assertIn("application ID changed externally", result.stderr)
-
def test_invalid_state_and_foreign_wrapper_fail_closed(self):
invalid = self.service.set("0", self.service.default_state())
self.assertFalse(invalid["success"])
invalid = self.service.set("123", {**self.service.default_state(), "dxvkFrameRate": 61})
self.assertFalse(invalid["success"])
- self.service.local_bin_dir.mkdir(parents=True, exist_ok=True)
self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8")
response = self.service.set("123", self.service.default_state())
self.assertFalse(response["success"])
self.assertIn("unowned", response["error"])
self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n")
- def test_remove_keeps_a_safe_owned_passthrough_wrapper(self):
+ def test_remove_keeps_safe_passthrough_wrapper(self):
self.service.set("123", self.service.default_state())
response = self.service.remove("123")
self.assertTrue(response["success"])