diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_flatpak_overrides.py | 158 | ||||
| -rw-r--r-- | tests/test_flatpak_service.py | 231 | ||||
| -rw-r--r-- | tests/test_steam_service.py | 77 | ||||
| -rw-r--r-- | tests/test_wrapper_service.py | 79 |
4 files changed, 386 insertions, 159 deletions
diff --git a/tests/test_flatpak_overrides.py b/tests/test_flatpak_overrides.py deleted file mode 100644 index ed3ef6a..0000000 --- a/tests/test_flatpak_overrides.py +++ /dev/null @@ -1,158 +0,0 @@ -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.modules.setdefault("tomllib", types.SimpleNamespace(loads=Mock())) -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) - -from lsfg_vk.flatpak_service import FlatpakService - - -class FlatpakOverrideTests(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.TemporaryDirectory() - home = Path(self.tempdir.name) / "home" / "deck" - home.mkdir(parents=True) - self.service = FlatpakService() - self.service.user_home = home - self.service.config_dir = home / ".config/lsfg-vk" - self.service.config_file_path = self.service.config_dir / "conf.toml" - self.service.legacy_script_path = home / "lsfg" - self.service.check_flatpak_available = Mock(return_value=True) - self.service._run_flatpak_command = Mock( - return_value=types.SimpleNamespace(returncode=0, stderr="", stdout="") - ) - self.app_id = "com.example.Game" - self.override_path = self.service._override_file_path(self.app_id) - - def tearDown(self): - self.tempdir.cleanup() - sys.modules.pop("lsfg_vk.plugin", None) - sys.modules.pop("lsfg_vk", None) - - def _paths(self): - return self.service._override_paths() - - def _write_override(self, content): - self.override_path.parent.mkdir(parents=True, exist_ok=True) - self.override_path.write_text(content, encoding="utf-8") - - def _show_response(self, content): - return types.SimpleNamespace(returncode=0, stderr="", stdout=content) - - def test_set_cleans_legacy_entries_and_verifies_readback(self): - paths = self._paths() - self._write_override( - "[Context]\n" - f"filesystems=/home/deck/keep;{paths['config_dir']}:rw;!{paths['legacy_home']};" - f"{paths['legacy_script']};{paths['legacy_dll']}:ro;{paths['dll_dir']}:ro;\n" - "unset-environment=KEEP_UNSET;LSFG_CONFIG;\n\n" - "[Environment]\n" - "KEEP_ENV=1\n" - "LSFG_CONFIG=\n" - "LSFGVK_CONFIG=old\n" - "ENABLE_GAMESCOPE_WSI=1\n" - "DXVK_HDR=1\n" - ) - expected = ( - "[Context]\n" - f"filesystems={paths['config_dir']}:rw;{paths['dll_dir']}:ro\n" - "[Environment]\n" - f"LSFGVK_CONFIG={paths['config_file']}\n" - "ENABLE_GAMESCOPE_WSI=0\n" - "DXVK_HDR=0\n" - ) - self.service._run_flatpak_command.side_effect = [ - self._show_response(""), - self._show_response(expected), - ] - - response = self.service.set_app_override(self.app_id) - command_args = self.service._run_flatpak_command.call_args_list[0].args[0] - cleaned = self.override_path.read_text(encoding="utf-8") - - self.assertTrue(response["success"]) - self.assertIn("--env=ENABLE_GAMESCOPE_WSI=0", command_args) - self.assertIn("--env=DXVK_HDR=0", command_args) - self.assertNotIn("--nofilesystem=/home/deck", command_args) - self.assertNotIn("--unset-env=LSFG_CONFIG", command_args) - self.assertIn("/home/deck/keep", cleaned) - self.assertIn("KEEP_ENV=1", cleaned) - self.assertNotIn("LSFG_CONFIG", cleaned) - self.assertNotIn(paths["legacy_home"], cleaned) - - def test_set_reports_failed_readback(self): - paths = self._paths() - self.service._run_flatpak_command.side_effect = [ - self._show_response(""), - self._show_response( - f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" - f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" - ), - ] - - response = self.service.set_app_override(self.app_id) - - self.assertFalse(response["success"]) - self.assertIn("verified", response["error"]) - - def test_remove_cleans_known_entries_preserves_unrelated_and_verifies(self): - paths = self._paths() - self._write_override( - "[Context]\n" - f"filesystems=/home/deck/keep;{paths['config_dir']};!{paths['legacy_home']};" - f"{paths['legacy_dll']};{paths['legacy_script']}\n" - "unset-environment=KEEP_UNSET;LSFG_CONFIG;ENABLE_GAMESCOPE_WSI\n\n" - "[Environment]\n" - "KEEP_ENV=1\n" - "LSFGVK_CONFIG=/old/path\n" - "DXVK_HDR=0\n" - ) - self.service._run_flatpak_command.side_effect = [ - self._show_response( - "[Context]\nfilesystems=/home/deck/keep\n" - "[Environment]\nKEEP_ENV=1\n" - ) - ] - - response = self.service.remove_app_override(self.app_id) - cleaned = self.override_path.read_text(encoding="utf-8") - - self.assertTrue(response["success"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 1) - self.assertIn("/home/deck/keep", cleaned) - self.assertIn("KEEP_UNSET", cleaned) - self.assertIn("KEEP_ENV", cleaned) - for name in ("LSFGVK_CONFIG", "LSFG_CONFIG", "ENABLE_GAMESCOPE_WSI", "DXVK_HDR"): - self.assertNotIn(name, cleaned) - for path in paths.values(): - if path != paths["config_file"]: - self.assertNotIn(path, cleaned) - - def test_remove_reports_failed_readback(self): - self._write_override("[Context]\nfilesystems=/home/deck/keep\n") - paths = self._paths() - self.service._run_flatpak_command.side_effect = [ - self._show_response( - f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" - f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" - "ENABLE_GAMESCOPE_WSI=0\nDXVK_HDR=0\n" - ) - ] - - response = self.service.remove_app_override(self.app_id) - - self.assertFalse(response["success"]) - self.assertIn("verified", response["error"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py new file mode 100644 index 0000000..38dfcb5 --- /dev/null +++ b/tests/test_flatpak_service.py @@ -0,0 +1,231 @@ +import json +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.flatpak_service import FlatpakService + + +class FlatpakServiceTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.home = Path(self.tempdir.name) / "home" / "deck" + self.home.mkdir(parents=True) + self.service = FlatpakService() + 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.check_flatpak_available = Mock(return_value=True) + self.service._run_flatpak_command = Mock() + 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) + + def tearDown(self): + self.tempdir.cleanup() + + @staticmethod + def _result(stdout="", returncode=0, stderr=""): + return types.SimpleNamespace(stdout=stdout, returncode=returncode, stderr=stderr) + + @staticmethod + def _extension_line(branch): + return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n" + + 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_resolve_reads_required_runtime_instead_of_any_installed_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("23.08")), + ] + + response = self.service.resolve_app_support("com.example.Game") + + self.assertTrue(response["success"]) + 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", "--runtime", "--columns=application,arch,branch"], + ) + + def test_install_records_only_a_new_user_owned_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + ] + + response = self.service.install_extension("24.08") + + self.assertTrue(response["success"]) + self.assertTrue(response["owned_by_plugin"]) + install_args = self.service._run_flatpak_command.call_args_list[1].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"]}, + ) + + def test_preexisting_branch_is_not_claimed_or_removed(self): + self.service._run_flatpak_command.return_value = self._result( + self._extension_line("24.08") + ) + + install_response = self.service.install_extension("24.08") + cleanup_response = self.service.remove_plugin_owned_extensions() + + self.assertTrue(install_response["success"]) + self.assertFalse(install_response["owned_by_plugin"]) + self.assertFalse(self.service.ownership_path.exists()) + self.assertTrue(cleanup_response["success"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 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") + + 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_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_ensure_app_support_installs_only_the_app_runtime_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(""), + self._result(""), + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + ] + + response = self.service.ensure_app_support("com.example.Game") + + self.assertTrue(response["success"]) + self.assertEqual(response["support_status"], "ready") + self.assertEqual(response["runtime_branch"], "24.08") + install_args = self.service._run_flatpak_command.call_args_list[4].args[0] + 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): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(""), + self._result(""), + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + ] + + 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"]}, + ) + + 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.service._run_flatpak_command.side_effect = [ + self._result( + "\n".join( + [ + "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "23.08"]), + "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]), + ] + ) + + "\n" + ), + self._result(""), + self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), + self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), + self._result(""), + self._result(""), + ] + + response = self.service.remove_plugin_owned_extensions() + + 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) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py new file mode 100644 index 0000000..849bb01 --- /dev/null +++ b/tests/test_steam_service.py @@ -0,0 +1,77 @@ +import sys +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.steam_service import SteamService, classify_shortcut_transport + + +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( + "/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"}, + ) + + def test_shortcut_data_preserves_transport_inputs(self): + game = SteamService._shortcut_game( + { + "appid": 123456, + "AppName": "PCSX2 shortcut", + "Exe": "/usr/bin/flatpak", + "LaunchOptions": "run net.pcsx2.PCSX2 --fullscreen", + "StartDir": "/home/deck/Games", + } + ) + + self.assertEqual(game["appid"], "123456") + self.assertEqual(game["transport"], { + "kind": "flatpak", + "flatpakAppId": "net.pcsx2.PCSX2", + }) + self.assertEqual(game["executable"], "/usr/bin/flatpak") + self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") + self.assertEqual(game["startDir"], "/home/deck/Games") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index f17a944..5010d23 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -123,10 +123,20 @@ class WrapperServiceTests(unittest.TestCase): encoding="utf-8", ) fake_flatpak.chmod(0o755) - self.service.set("123", self._state(dxvkFrameRate=20, enableZink=True), str(fake_flatpak)) + 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) @@ -137,6 +147,73 @@ class WrapperServiceTests(unittest.TestCase): 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_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"]) |
