diff --git a/gamelauncher_test.py b/gamelauncher_test.py deleted file mode 100644 index 0589157ab..000000000 --- a/gamelauncher_test.py +++ /dev/null @@ -1,1396 +0,0 @@ -import unittest -import gamelauncher -import os -import argparse -from argparse import Namespace -from unittest.mock import patch -from pathlib import Path -from tomllib import TOMLDecodeError -from shutil import rmtree -import re -import gamelauncher_plugins - - -class TestGameLauncher(unittest.TestCase): - """Test suite for gamelauncher.py. - - TODO: test for mutually exclusive options - """ - - def setUp(self): - """Create the test directory, exe and environment variables.""" - self.env = { - "WINEPREFIX": "", - "GAMEID": "", - "PROTON_CRASH_REPORT_DIR": "/tmp/ULWGL_crashreports", - "PROTONPATH": "", - "STEAM_COMPAT_APP_ID": "", - "STEAM_COMPAT_TOOL_PATHS": "", - "STEAM_COMPAT_LIBRARY_PATHS": "", - "STEAM_COMPAT_MOUNTS": "", - "STEAM_COMPAT_INSTALL_PATH": "", - "STEAM_COMPAT_CLIENT_INSTALL_PATH": "", - "STEAM_COMPAT_DATA_PATH": "", - "STEAM_COMPAT_SHADER_PATH": "", - "FONTCONFIG_PATH": "", - "EXE": "", - "SteamAppId": "", - "SteamGameId": "", - "STEAM_RUNTIME_LIBRARY_PATH": "", - "ULWGL_ID": "", - } - self.test_opts = "-foo -bar" - # Proton verb - # Used when testing build_command - self.test_verb = "waitforexitandrun" - # Test directory - self.test_file = "./tmp.WMYQiPb9A" - # Executable - self.test_exe = self.test_file + "/" + "foo" - Path(self.test_file).mkdir(exist_ok=True) - Path(self.test_exe).touch() - - def tearDown(self): - """Unset environment variables and delete test files after each test.""" - for key, val in self.env.items(): - if key in os.environ: - os.environ.pop(key) - - if Path(self.test_file).exists(): - rmtree(self.test_file) - - def test_game_drive_empty(self): - """Test enable_steam_game_drive. - - Empty WINE prefixes can be created by passing an empty string to --exe - During this process, we attempt to prepare setting up game drive and set the values for STEAM_RUNTIME_LIBRARY_PATH and STEAM_COMPAT_INSTALL_PATHS - The resulting value of those variables should be colon delimited string with no leading colons and contain only /usr/lib or /usr/lib32 - """ - result = None - result_set_env = None - result_check_env = None - result_gamedrive = None - Path(self.test_file + "/proton").touch() - - # Replicate main's execution and test up until enable_steam_game_drive - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=""), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - # Parse arguments - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - # Check if required env var are set - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - - # Set the required environment variables - result_set_env = gamelauncher.set_env(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - - # Check for expected changes - # We only check the required ones - self.assertEqual(result_set_env["WINEPREFIX"], self.test_file) - self.assertEqual(result_set_env["PROTONPATH"], self.test_file) - self.assertEqual(result_set_env["GAMEID"], self.test_file) - # Check if the EXE is empty - self.assertFalse(result_set_env["EXE"], "Expected EXE to be empty") - - self.env["ULWGL_ID"] = self.env["GAMEID"] - self.env["STEAM_COMPAT_APP_ID"] = "0" - - if re.match(r"^ulwgl-[\d\w]+$", self.env["ULWGL_ID"]): - self.env["STEAM_COMPAT_APP_ID"] = self.env["ULWGL_ID"][ - self.env["ULWGL_ID"].find("-") + 1 : - ] - - self.env["SteamAppId"] = self.env["STEAM_COMPAT_APP_ID"] - self.env["SteamGameId"] = self.env["SteamAppId"] - self.env["WINEPREFIX"] = Path(self.env["WINEPREFIX"]).expanduser().as_posix() - self.env["PROTONPATH"] = Path(self.env["PROTONPATH"]).expanduser().as_posix() - self.env["STEAM_COMPAT_DATA_PATH"] = self.env["WINEPREFIX"] - self.env["STEAM_COMPAT_SHADER_PATH"] = ( - self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - ) - self.env["STEAM_COMPAT_INSTALL_PATH"] = ( - Path(self.env["EXE"]).parent.expanduser().as_posix() - ) - self.env["EXE"] = Path(self.env["EXE"]).expanduser().as_posix() - self.env["STEAM_COMPAT_TOOL_PATHS"] = ( - self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - self.env["STEAM_COMPAT_MOUNTS"] = self.env["STEAM_COMPAT_TOOL_PATHS"] - - if not getattr(result, "exe", None) and not getattr(result, "config", None): - self.env["EXE"] = "" - self.env["STEAM_COMPAT_INSTALL_PATH"] = "" - self.verb = "waitforexitandrun" - - # Game Drive - result_gamedrive = gamelauncher_plugins.enable_steam_game_drive(self.env) - self.assertTrue(result_gamedrive is self.env, "Expected the same reference") - - self.assertTrue( - self.env["STEAM_RUNTIME_LIBRARY_PATH"], - "Expected two elements in STEAM_RUNTIME_LIBRARY_PATHS", - ) - - # We just expect /usr/lib and /usr/lib32 - self.assertEqual( - len(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":")), - 2, - "Expected two values in STEAM_RUNTIME_LIBRARY_PATH", - ) - - # We need to sort the elements because the values were originally in a set - str1, str2 = [*sorted(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":"))] - - # Check that there are no trailing colons or unexpected characters - self.assertEqual(str1, "/usr/lib", "Expected /usr/lib") - self.assertEqual(str2, "/usr/lib32", "Expected /usr/lib32") - - # Both of these values should be empty still after calling enable_steam_game_drive - self.assertFalse( - self.env["STEAM_COMPAT_INSTALL_PATH"], - "Expected STEAM_COMPAT_INSTALL_PATH to be empty when passing an empty EXE", - ) - self.assertFalse(self.env["EXE"], "Expected EXE to be empty on empty string") - - def test_build_command_verb(self): - """Test build_command. - - An error should not be raised if we pass a Proton verb we don't expect - By default, we use "waitforexitandrun" for a verb we don't expect - Currently we only expect: - "waitforexitandrun" - "run" - "runinprefix" - "destroyprefix" - "getcompatpath" - "getnativepath" - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "{self.test_exe}" - """ - toml_path = self.test_file + "/" + test_toml - result = None - result_set_env = None - test_command = [] - test_verb = "foo" - Path(self.test_file + "/proton").touch() - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path, verb=test_verb), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - # Check if a verb was passed - self.assertTrue(vars(result).get("verb"), "Expected a value for --verb") - result_set_env = gamelauncher.set_env_toml(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - # Check for changes after calling - self.assertEqual( - result_set_env["EXE"], - self.test_exe + " " + self.test_file + " " + self.test_file, - ) - self.assertEqual(result_set_env["WINEPREFIX"], self.test_file) - self.assertEqual(result_set_env["PROTONPATH"], self.test_file) - self.assertEqual(result_set_env["GAMEID"], self.test_file) - - self.env["ULWGL_ID"] = self.env["GAMEID"] - self.env["STEAM_COMPAT_APP_ID"] = "0" - - if re.match(r"^ulwgl-[\d\w]+$", self.env["ULWGL_ID"]): - self.env["STEAM_COMPAT_APP_ID"] = self.env["ULWGL_ID"][ - self.env["ULWGL_ID"].find("-") + 1 : - ] - - self.env["SteamAppId"] = self.env["STEAM_COMPAT_APP_ID"] - self.env["SteamGameId"] = self.env["SteamAppId"] - self.env["WINEPREFIX"] = Path(self.env["WINEPREFIX"]).expanduser().as_posix() - self.env["PROTONPATH"] = Path(self.env["PROTONPATH"]).expanduser().as_posix() - self.env["STEAM_COMPAT_DATA_PATH"] = self.env["WINEPREFIX"] - self.env["STEAM_COMPAT_SHADER_PATH"] = ( - self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - ) - self.env["STEAM_COMPAT_INSTALL_PATH"] = ( - Path(self.env["EXE"]).parent.expanduser().as_posix() - ) - self.env["EXE"] = Path(self.env["EXE"]).expanduser().as_posix() - self.env["STEAM_COMPAT_TOOL_PATHS"] = ( - self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - self.env["STEAM_COMPAT_MOUNTS"] = self.env["STEAM_COMPAT_TOOL_PATHS"] - - # Create an empty Proton prefix when asked - if not getattr(result, "exe", None) and not getattr(result, "config", None): - self.env["EXE"] = "" - self.env["STEAM_COMPAT_INSTALL_PATH"] = "" - self.verb = "waitforexitandrun" - - for key, val in self.env.items(): - os.environ[key] = val - test_command = gamelauncher.build_command(self.env, test_command, test_verb) - # The verb should be 2nd in the array - self.assertIsInstance(test_command, list, "Expected a List from build_command") - self.assertTrue(test_command[2], self.test_verb) - - def test_build_command_nofile(self): - """Test build_command. - - A FileNotFoundError should be raised if $PROTONPATH/proton does not exist - Just test the TOML case for the coverage - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "{self.test_exe}" - """ - toml_path = self.test_file + "/" + test_toml - result = None - result_set_env = None - test_command = [] - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - result_set_env = gamelauncher.set_env_toml(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - # Check for changes after calling - self.assertEqual( - result_set_env["EXE"], - self.test_exe + " " + self.test_file + " " + self.test_file, - ) - self.assertEqual(result_set_env["WINEPREFIX"], self.test_file) - self.assertEqual(result_set_env["PROTONPATH"], self.test_file) - self.assertEqual(result_set_env["GAMEID"], self.test_file) - - self.env["ULWGL_ID"] = self.env["GAMEID"] - self.env["STEAM_COMPAT_APP_ID"] = "0" - - if re.match(r"^ulwgl-[\d\w]+$", self.env["ULWGL_ID"]): - self.env["STEAM_COMPAT_APP_ID"] = self.env["ULWGL_ID"][ - self.env["ULWGL_ID"].find("-") + 1 : - ] - - self.env["SteamAppId"] = self.env["STEAM_COMPAT_APP_ID"] - self.env["SteamGameId"] = self.env["SteamAppId"] - self.env["WINEPREFIX"] = Path(self.env["WINEPREFIX"]).expanduser().as_posix() - self.env["PROTONPATH"] = Path(self.env["PROTONPATH"]).expanduser().as_posix() - self.env["STEAM_COMPAT_DATA_PATH"] = self.env["WINEPREFIX"] - self.env["STEAM_COMPAT_SHADER_PATH"] = ( - self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - ) - self.env["STEAM_COMPAT_INSTALL_PATH"] = ( - Path(self.env["EXE"]).parent.expanduser().as_posix() - ) - self.env["EXE"] = Path(self.env["EXE"]).expanduser().as_posix() - self.env["STEAM_COMPAT_TOOL_PATHS"] = ( - self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - self.env["STEAM_COMPAT_MOUNTS"] = self.env["STEAM_COMPAT_TOOL_PATHS"] - # Create an empty Proton prefix when asked - if not getattr(result, "exe", None) and not getattr(result, "config", None): - self.env["EXE"] = "" - self.env["STEAM_COMPAT_INSTALL_PATH"] = "" - self.verb = "waitforexitandrun" - - for key, val in self.env.items(): - os.environ[key] = val - with self.assertRaisesRegex(FileNotFoundError, "proton"): - gamelauncher.build_command(self.env, test_command, self.test_verb) - - def test_build_command_toml(self): - """Test build_command. - - After parsing a valid TOML file, be sure we do not raise a FileNotFoundError - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "{self.test_exe}" - """ - toml_path = self.test_file + "/" + test_toml - result = None - result_set_env = None - test_command = [] - - Path(self.test_file + "/proton").touch() - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - result_set_env = gamelauncher.set_env_toml(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - # Check for changes after calling - self.assertEqual( - result_set_env["EXE"], - self.test_exe + " " + self.test_file + " " + self.test_file, - ) - self.assertEqual(result_set_env["WINEPREFIX"], self.test_file) - self.assertEqual(result_set_env["PROTONPATH"], self.test_file) - self.assertEqual(result_set_env["GAMEID"], self.test_file) - - self.env["ULWGL_ID"] = self.env["GAMEID"] - self.env["STEAM_COMPAT_APP_ID"] = "0" - - if re.match(r"^ulwgl-[\d\w]+$", self.env["ULWGL_ID"]): - self.env["STEAM_COMPAT_APP_ID"] = self.env["ULWGL_ID"][ - self.env["ULWGL_ID"].find("-") + 1 : - ] - - self.env["SteamAppId"] = self.env["STEAM_COMPAT_APP_ID"] - self.env["SteamGameId"] = self.env["SteamAppId"] - self.env["WINEPREFIX"] = Path(self.env["WINEPREFIX"]).expanduser().as_posix() - self.env["PROTONPATH"] = Path(self.env["PROTONPATH"]).expanduser().as_posix() - self.env["STEAM_COMPAT_DATA_PATH"] = self.env["WINEPREFIX"] - self.env["STEAM_COMPAT_SHADER_PATH"] = ( - self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - ) - self.env["STEAM_COMPAT_INSTALL_PATH"] = ( - Path(self.env["EXE"]).parent.expanduser().as_posix() - ) - self.env["EXE"] = Path(self.env["EXE"]).expanduser().as_posix() - self.env["STEAM_COMPAT_TOOL_PATHS"] = ( - self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - self.env["STEAM_COMPAT_MOUNTS"] = self.env["STEAM_COMPAT_TOOL_PATHS"] - - # Create an empty Proton prefix when asked - if not getattr(result, "exe", None) and not getattr(result, "config", None): - self.env["EXE"] = "" - self.env["STEAM_COMPAT_INSTALL_PATH"] = "" - self.verb = "waitforexitandrun" - - for key, val in self.env.items(): - os.environ[key] = val - test_command = gamelauncher.build_command( - self.env, test_command, self.test_verb - ) - self.assertIsInstance(test_command, list, "Expected a List from build_command") - # Verify contents - entry_point, opt1, verb, opt2, proton, verb2, exe = [*test_command] - # The entry point dest could change. Just check if there's a value - self.assertTrue(entry_point, "Expected an entry point") - self.assertEqual(opt1, "--verb", "Expected --verb") - self.assertEqual(verb, self.test_verb, "Expected a verb") - self.assertEqual(opt2, "--", "Expected --") - self.assertEqual( - proton, - Path(self.env.get("PROTONPATH") + "/proton").as_posix(), - "Expected the proton file", - ) - self.assertEqual(verb2, self.test_verb, "Expected a verb") - self.assertEqual(exe, self.env["EXE"], "Expected the EXE") - - def test_build_command(self): - """Test build_command. - - After parsing valid environment variables set by the user, be sure we do not raise a FileNotFoundError - """ - result_args = None - result_check_env = None - test_command = [] - - # Mock the /proton file - Path(self.test_file + "/proton").touch() - - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, options=self.test_opts), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance( - result_args, Namespace, "parse_args did not return a Namespace" - ) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result_set_env = gamelauncher.set_env(self.env, result_args) - - # Check for changes after calling - self.assertEqual(result_set_env["WINEPREFIX"], self.test_file) - self.assertEqual(result_set_env["PROTONPATH"], self.test_file) - self.assertEqual(result_set_env["GAMEID"], self.test_file) - # Test for expected EXE with options - self.assertEqual( - self.env.get("EXE"), - "{} {}".format(self.test_exe, self.test_opts), - "Expected the concat EXE and game options to not have trailing spaces", - ) - - self.env["ULWGL_ID"] = self.env["GAMEID"] - self.env["STEAM_COMPAT_APP_ID"] = "0" - - if re.match(r"^ulwgl-[\d\w]+$", self.env["ULWGL_ID"]): - self.env["STEAM_COMPAT_APP_ID"] = self.env["ULWGL_ID"][ - self.env["ULWGL_ID"].find("-") + 1 : - ] - - self.env["SteamAppId"] = self.env["STEAM_COMPAT_APP_ID"] - self.env["SteamGameId"] = self.env["SteamAppId"] - self.env["WINEPREFIX"] = Path(self.env["WINEPREFIX"]).expanduser().as_posix() - self.env["PROTONPATH"] = Path(self.env["PROTONPATH"]).expanduser().as_posix() - self.env["STEAM_COMPAT_DATA_PATH"] = self.env["WINEPREFIX"] - self.env["STEAM_COMPAT_SHADER_PATH"] = ( - self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - ) - self.env["STEAM_COMPAT_INSTALL_PATH"] = ( - Path(self.env["EXE"]).parent.expanduser().as_posix() - ) - self.env["EXE"] = Path(self.env["EXE"]).expanduser().as_posix() - self.env["STEAM_COMPAT_TOOL_PATHS"] = ( - self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - self.env["STEAM_COMPAT_MOUNTS"] = self.env["STEAM_COMPAT_TOOL_PATHS"] - - # Create an empty Proton prefix when asked - if not getattr(result_args, "exe", None) and not getattr( - result_args, "config", None - ): - self.env["EXE"] = "" - self.env["STEAM_COMPAT_INSTALL_PATH"] = "" - self.verb = "waitforexitandrun" - - for key, val in self.env.items(): - os.environ[key] = val - - test_command = gamelauncher.build_command( - self.env, test_command, self.test_verb - ) - self.assertIsInstance(test_command, list, "Expected a List from build_command") - self.assertEqual( - len(test_command), 7, "Expected 7 elements in the list from build_command" - ) - # Verify contents - entry_point, opt1, verb, opt2, proton, verb2, exe = [*test_command] - # The entry point dest could change. Just check if there's a value - self.assertTrue(entry_point, "Expected an entry point") - self.assertEqual(opt1, "--verb", "Expected --verb") - self.assertEqual(verb, self.test_verb, "Expected a verb") - self.assertEqual(opt2, "--", "Expected --") - self.assertEqual( - proton, - Path(self.env.get("PROTONPATH") + "/proton").as_posix(), - "Expected the proton file", - ) - self.assertEqual(verb2, self.test_verb, "Expected a verb") - self.assertEqual(exe, self.env["EXE"], "Expected the EXE") - - def test_set_env_toml_config(self): - """Test set_env_toml when passing a configuration file. - - An FileNotFoundError should be raised when passing a TOML file that doesn't exist - """ - test_file = "foo.toml" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=test_file), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - with self.assertRaisesRegex(FileNotFoundError, test_file): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_opts_nofile(self): - """Test set_env_toml for options that are a file. - - An error should not be raised if a launch argument is a file - We allow this behavior to give users flexibility at the cost of security - """ - test_toml = "foo.toml" - toml_path = self.test_file + "/" + test_toml - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{toml_path}"] - exe = "{self.test_exe}" - """ - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - gamelauncher.set_env_toml(self.env, result) - # Check if the TOML file we just created - self.assertTrue( - Path(self.env["EXE"].split(" ")[1]).is_file(), - "Expected a file to be appended to the executable", - ) - - def test_set_env_toml_nofile(self): - """Test set_env_toml for values that are not a file. - - A FileNotFoundError should be raised if the 'exe' is not a file - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "./bar" - """ - toml_path = self.test_file + "/" + test_toml - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - with self.assertRaisesRegex(FileNotFoundError, "exe"): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_empty(self): - """Test set_env_toml for empty values not required by parse_args. - - A ValueError should be thrown if 'game_id' is empty - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "{self.test_file}" - """ - toml_path = self.test_file + "/" + test_toml - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - with self.assertRaisesRegex(ValueError, "game_id"): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_err(self): - """Test set_env_toml for valid TOML. - - A TOMLDecodeError should be raised for invalid values - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = [[ - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - """ - toml_path = self.test_file + "/" + test_toml - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - with self.assertRaisesRegex(TOMLDecodeError, "Invalid"): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_nodir(self): - """Test set_env_toml if certain key/value are not a dir. - - An IsDirectoryError should be raised if proton or prefix are not directories - """ - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "foo" - proton = "foo" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - """ - toml_path = self.test_file + "/" + test_toml - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - with self.assertRaisesRegex(NotADirectoryError, "prefix"): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_tables(self): - """Test set_env_toml for expected tables. - - A KeyError should be raised if the table 'ulwgl' is absent - """ - test_toml = "foo.toml" - toml_str = f""" - [foo] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - """ - toml_path = self.test_file + "/" + test_toml - result = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - with self.assertRaisesRegex(KeyError, "ulwgl"): - gamelauncher.set_env_toml(self.env, result) - - def test_set_env_toml_paths(self): - """Test set_env_toml when specifying unexpanded file path values in the config file. - - Example: ~/Games/foo.exe - An error should not be raised when passing unexpanded paths to the config file as well as the prefix, proton and exe keys - """ - test_toml = "foo.toml" - pattern = r"^/home/[a-zA-Z]+" - - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file - path_to_tmp = Path( - Path(__file__).cwd().as_posix() + "/" + self.test_file - ).as_posix() - path_to_exe = Path( - Path(__file__).cwd().as_posix() + "/" + self.test_exe - ).as_posix() - - # Replace /home/[a-zA-Z]+ substring in path with tilda - unexpanded_path = re.sub( - pattern, - "~", - path_to_tmp, - ) - unexpanded_exe = re.sub( - pattern, - "~", - path_to_exe, - ) - toml_str = f""" - [ulwgl] - prefix = "{unexpanded_path}" - proton = "{unexpanded_path}" - game_id = "{unexpanded_path}" - exe = "{unexpanded_exe}" - """ - # Path to TOML in unexpanded form - toml_path = unexpanded_path + "/" + test_toml - result = None - result_set_env = None - - Path(toml_path).expanduser().touch() - - with Path(toml_path).expanduser().open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - result_set_env = gamelauncher.set_env_toml(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - # Check that the paths are still in the unexpanded form - # In main, we only expand them after this function exits to prepare for building the command - self.assertEqual( - self.env["EXE"], unexpanded_exe, "Expected path not to be expanded" - ) - self.assertEqual( - self.env["PROTONPATH"], - unexpanded_path, - "Expected path not to be expanded", - ) - self.assertEqual( - self.env["WINEPREFIX"], - unexpanded_path, - "Expected path not to be expanded", - ) - self.assertEqual( - self.env["GAMEID"], unexpanded_path, "Expectd path not to be expanded" - ) - - def test_set_env_toml(self): - """Test set_env_toml.""" - test_toml = "foo.toml" - toml_str = f""" - [ulwgl] - prefix = "{self.test_file}" - proton = "{self.test_file}" - game_id = "{self.test_file}" - launch_args = ["{self.test_file}", "{self.test_file}"] - exe = "{self.test_exe}" - """ - toml_path = self.test_file + "/" + test_toml - result = None - result_set_env = None - - Path(toml_path).touch() - - with Path(toml_path).open(mode="w") as file: - file.write(toml_str) - - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=toml_path), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertTrue(vars(result).get("config"), "Expected a value for --config") - result_set_env = gamelauncher.set_env_toml(self.env, result) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - - def test_set_env_exe_nofile(self): - """Test set_env when setting no options via --options and appending options to --exe. - - gamelauncher.py --exe "foo -bar" - Options can be appended at the end of the exe if wrapping the value in quotes - No error should be raised if the --exe passed by the user doesn't exist - We trust the user that its legit and only validate the EXE in the TOML case - """ - result_args = None - result_check_env = None - result_set_env = None - - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe + " foo"), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance( - result_args, Namespace, "parse_args did not return a Namespace" - ) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result_set_env = gamelauncher.set_env(self.env, result_args) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["EXE"], - self.test_exe + " foo", - "Expected EXE to be set after passing garbage", - ) - self.assertTrue(Path(self.test_exe).exists(), "Expected the EXE to exist") - self.assertFalse( - Path(self.test_exe + " foo").exists(), - "Expected the concat of EXE and options to not exist", - ) - - def test_set_env_opts_nofile(self): - """Test set_env when an exe's options is a file. - - We allow options that may or may not be legit - No error should be raised in this case and we just check if options are a file - """ - result_args = None - result_check_env = None - result_set_env = None - - # File that will be passed as an option to the exe - test_opts_file = "baz" - Path(test_opts_file).touch() - - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, options=test_opts_file), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance( - result_args, Namespace, "parse_args did not return a Namespace" - ) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result_set_env = gamelauncher.set_env(self.env, result_args) - self.assertTrue(result_set_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["EXE"], - self.test_exe + " " + test_opts_file, - "Expected EXE to be set after appending a file as an option", - ) - # The concat of exe and options shouldn't be a file - self.assertFalse( - Path(self.env["EXE"]).is_file(), - "Expected EXE to not be a file when passing options", - ) - # However each part is a file - self.assertTrue( - Path(test_opts_file).is_file(), - "Expected a file for this test to be used as an option", - ) - self.assertTrue( - Path(self.test_exe).is_file(), - "Expected a file for this test to be used as an option", - ) - Path(test_opts_file).unlink() - - def test_set_env_opts(self): - """Test set_env. - - Ensure no failures and verify that $EXE is set with options passed - """ - result_args = None - result_check_env = None - result = None - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, options=self.test_opts), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance( - result_args, Namespace, "parse_args did not return a Namespace" - ) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result = gamelauncher.set_env(self.env, result_args) - self.assertIsInstance(result, dict, "Expected a Dictionary from set_env") - self.assertTrue(self.env.get("EXE"), "Expected EXE to not be empty") - self.assertEqual( - self.env.get("EXE"), - self.test_exe + " " + self.test_opts, - "Expected EXE to not have trailing spaces", - ) - - def test_set_env_exe(self): - """Test set_env. - - Ensure no failures and verify that $EXE - """ - result_args = None - result_check_env = None - result = None - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance( - result_args, Namespace, "parse_args did not return a Namespace" - ) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result = gamelauncher.set_env(self.env, result_args) - self.assertTrue(result is self.env, "Expected the same reference") - self.assertTrue(self.env.get("EXE"), "Expected EXE to not be empty") - - def test_set_env(self): - """Test set_env. - - Ensure no failures when passing --exe and setting $WINEPREFIX and $PROTONPATH - """ - result_args = None - result_check_env = None - result = None - # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= gamelauncher --exe=... - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(game=self.test_file), - ): - os.environ["WINEPREFIX"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - os.environ["GAMEID"] = self.test_file - result_args = gamelauncher.parse_args() - self.assertIsInstance(result_args, Namespace) - result_check_env = gamelauncher.check_env(self.env) - self.assertTrue(result_check_env is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - result = gamelauncher.set_env(self.env, result_args) - self.assertTrue(result is self.env, "Expected the same reference") - - def test_setup_pfx_symlinks(self): - """Test _setup_pfx for valid symlinks. - - Ensure that symbolic links to the WINE prefix (pfx) are always in expanded form when passed an unexpanded path. - For example: - if WINEPREFIX is /home/foo/.wine - pfx -> /home/foo/.wine - - We do not want the symbolic link such as: - pfx -> ~/.wine - """ - result = None - pattern = r"^/home/[a-zA-Z]+" - unexpanded_path = re.sub( - pattern, - "~", - Path( - Path(self.test_file).cwd().as_posix() + "/" + self.test_file - ).as_posix(), - ) - result = gamelauncher._setup_pfx(unexpanded_path) - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file - self.assertIsNone( - result, - "Expected None when creating symbolic link to WINE prefix and tracked_files file", - ) - self.assertTrue( - Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" - ) - self.assertTrue( - Path(self.test_file + "/tracked_files").is_file(), - "Expected tracked_files to be a file", - ) - self.assertTrue( - Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" - ) - # Check if the symlink is in its unexpanded form - self.assertEqual( - Path(self.test_file + "/pfx").readlink().as_posix(), - Path(unexpanded_path).expanduser().as_posix(), - ) - - def test_setup_pfx_paths(self): - """Test _setup_pfx on unexpanded paths. - - An error should not be raised when passing paths such as ~/path/to/prefix. - """ - result = None - pattern = r"^/home/[a-zA-Z]+" - unexpanded_path = re.sub( - pattern, - "~", - Path(Path(self.test_file).as_posix()).as_posix(), - ) - result = gamelauncher._setup_pfx(unexpanded_path) - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file - self.assertIsNone( - result, - "Expected None when creating symbolic link to WINE prefix and tracked_files file", - ) - self.assertTrue( - Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" - ) - self.assertTrue( - Path(self.test_file + "/tracked_files").is_file(), - "Expected tracked_files to be a file", - ) - - def test_setup_pfx(self): - """Test _setup_pfx.""" - result = None - result = gamelauncher._setup_pfx(self.test_file) - self.assertIsNone( - result, - "Expected None when creating symbolic link to WINE prefix and tracked_files file", - ) - self.assertTrue( - Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" - ) - self.assertTrue( - Path(self.test_file + "/tracked_files").is_file(), - "Expected tracked_files to be a file", - ) - - def test_parse_args_verb(self): - """Test parse_args --verb.""" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, verb=self.test_verb), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertEqual( - result.verb, - self.test_verb, - "Expected the same value when setting --verb", - ) - - def test_parse_args_store(self): - """Test parse_args --store.""" - test_store = "gog" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, store=test_store), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertEqual( - result.store, - test_store, - "Expected the same value when setting --store", - ) - - def test_parse_args_options(self): - """Test parse_args --options.""" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_exe, options=self.test_opts), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - self.assertEqual( - result.options, - self.test_opts, - "Expected the same value when setting --options", - ) - - def test_parse_args(self): - """Test parse_args with no options. - - There's a requirement to create an empty prefix - A SystemExit should be raised in this case: - ./gamelauncher.py - """ - with self.assertRaises(SystemExit): - gamelauncher.parse_args() - - def test_parse_args_config(self): - """Test parse_args --config.""" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(config=self.test_file), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - - def test_parse_args_game(self): - """Test parse_args --exe.""" - with patch.object( - gamelauncher, - "parse_args", - return_value=argparse.Namespace(exe=self.test_file), - ): - result = gamelauncher.parse_args() - self.assertIsInstance( - result, Namespace, "Expected a Namespace from parse_arg" - ) - - def test_env_proton_dir(self): - """Test check_env when $PROTONPATH is not a directory. - - An ValueError should occur if the value is not a directory - """ - with self.assertRaisesRegex(ValueError, "PROTONPATH"): - os.environ["WINEPREFIX"] = self.test_file - os.environ["GAMEID"] = self.test_file - os.environ["PROTONPATH"] = "./foo" - gamelauncher.check_env(self.env) - self.assertFalse( - Path(os.environ["PROTONPATH"]).is_dir(), - "Expected PROTONPATH to not be a directory", - ) - - def test_env_wine_dir(self): - """Test check_env when $WINEPREFIX is not a directory. - - An error should not be raised if a WINEPREFIX is set but the path has not been created. - """ - os.environ["WINEPREFIX"] = "./foo" - os.environ["GAMEID"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - gamelauncher.check_env(self.env) - self.assertEqual( - Path(os.environ["WINEPREFIX"]).is_dir(), - True, - "Expected WINEPREFIX to be created if not already exist", - ) - if Path(os.environ["WINEPREFIX"]).is_dir(): - rmtree(os.environ["WINEPREFIX"]) - - def test_env_vars_paths(self): - """Test check_env when setting unexpanded paths for $WINEPREFIX and $PROTONPATH.""" - # Replaces the expanded path to unexpanded - # Example: ~/some/path/to/this/file - pattern = r"^/home/[a-zA-Z]+" - path_to_tmp = Path( - Path(__file__).cwd().as_posix() + "/" + self.test_file - ).as_posix() - - # Replace /home/[a-zA-Z]+ substring in path with tilda - unexpanded_path = re.sub( - pattern, - "~", - path_to_tmp, - ) - - result = None - os.environ["WINEPREFIX"] = unexpanded_path - os.environ["GAMEID"] = self.test_file - os.environ["PROTONPATH"] = unexpanded_path - result = gamelauncher.check_env(self.env) - self.assertTrue(result is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], unexpanded_path, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], unexpanded_path, "Expected PROTONPATH to be set" - ) - - def test_env_vars(self): - """Test check_env when setting $WINEPREFIX, $GAMEID and $PROTONPATH.""" - result = None - os.environ["WINEPREFIX"] = self.test_file - os.environ["GAMEID"] = self.test_file - os.environ["PROTONPATH"] = self.test_file - result = gamelauncher.check_env(self.env) - self.assertTrue(result is self.env, "Expected the same reference") - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - self.assertEqual( - self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" - ) - - def test_env_vars_proton(self): - """Test check_env when setting only $WINEPREFIX and $GAMEID.""" - with self.assertRaisesRegex(ValueError, "PROTONPATH"): - os.environ["WINEPREFIX"] = self.test_file - os.environ["GAMEID"] = self.test_file - gamelauncher.check_env(self.env) - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - self.assertEqual( - self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" - ) - - def test_env_vars_wine(self): - """Test check_env when setting only $WINEPREFIX.""" - with self.assertRaisesRegex(ValueError, "GAMEID"): - os.environ["WINEPREFIX"] = self.test_file - gamelauncher.check_env(self.env) - self.assertEqual( - self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" - ) - - def test_env_vars_none(self): - """Tests check_env when setting no env vars.""" - with self.assertRaisesRegex(ValueError, "WINEPREFIX"): - gamelauncher.check_env(self.env) - - -if __name__ == "__main__": - unittest.main() diff --git a/ulwgl-run b/ulwgl-run deleted file mode 100755 index 2ca111669..000000000 --- a/ulwgl-run +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/sh - -# use for debug only. -# set -x - -if [ -z "$1" ] || [ -z "$WINEPREFIX" ] || [ -z "$GAMEID" ] || [ -z "$PROTONPATH" ]; then - echo "Usage: WINEPREFIX= GAMEID= PROTONPATH= ./gamelauncher.sh " - echo "Ex:" - echo "WINEPREFIX=$HOME/Games/epic-games-store GAMEID=egs PROTONPATH=\"$HOME/.steam/steam/compatibilitytools.d/GE-Proton8-28\" ./gamelauncher.sh \"$HOME/Games/epic-games-store/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe\" \"-opengl -SkipBuildPatchPrereq\"" - exit 1 -fi - -me="$(readlink -f "$0")" -here="${me%/*}" - -if [ "$WINEPREFIX" ]; then - if [ ! -d "$WINEPREFIX" ]; then - mkdir -p "$WINEPREFIX" - export PROTON_DLL_COPY="*" - fi - if [ ! -d "$WINEPREFIX"/pfx ]; then - ln -s "$WINEPREFIX" "$WINEPREFIX"/pfx > /dev/null 2>&1 - fi - if [ ! -f "$WINEPREFIX"/tracked_files ]; then - touch "$WINEPREFIX"/tracked_files - fi - if [ ! -f "$WINEPREFIX/dosdevices/" ]; then - mkdir -p "$WINEPREFIX"/dosdevices - ln -s "../drive_c" "$WINEPREFIX/dosdevices/c:" > /dev/null 2>&1 - fi -fi - -if [ -n "$PROTONPATH" ]; then - if [ ! -d "$PROTONPATH" ]; then - echo "ERROR: $PROTONPATH is invalid, aborting!" - exit 1 - fi -fi - -export ULWGL_ID="$GAMEID" -export STEAM_COMPAT_APP_ID="0" -numcheck='^[0-9]+$' -if echo "$ULWGL_ID" | cut -d "-" -f 2 | grep -Eq "$numcheck"; then - STEAM_COMPAT_APP_ID=$(echo "$ULWGL_ID" | cut -d "-" -f 2) - export STEAM_COMPAT_APP_ID -fi -export SteamAppId="$STEAM_COMPAT_APP_ID" -export SteamGameId="$STEAM_COMPAT_APP_ID" - -# TODO: Ideally this should be the main game install path, which is often, but not always the path of the game's executable. -if [ -z "$STEAM_COMPAT_INSTALL_PATH" ]; then - exepath="$(readlink -f "$1")" - gameinstallpath="${exepath%/*}" - export STEAM_COMPAT_INSTALL_PATH="$gameinstallpath" -fi - -compat_lib_path=$(findmnt -T "$STEAM_COMPAT_INSTALL_PATH" | tail -n 1 | awk '{ print $1 }') -if [ "$compat_lib_path" != "/" ]; then - export STEAM_COMPAT_LIBRARY_PATHS="${STEAM_COMPAT_LIBRARY_PATHS:+"${STEAM_COMPAT_LIBRARY_PATHS}:"}$compat_lib_path" -fi - -if [ -z "$STEAM_RUNTIME_LIBRARY_PATH" ]; then - # The following info taken from steam ~/.local/share/ubuntu12_32/steam-runtime/run.sh - host_library_paths= - exit_status=0 - ldconfig_output=$(/sbin/ldconfig -XNv 2> /dev/null; exit $?) || exit_status=$? - if [ $exit_status != 0 ]; then - echo "Warning: An unexpected error occurred while executing \"/sbin/ldconfig -XNv\", the exit status was $exit_status" - fi - - while read -r line; do - # If line starts with a leading / and contains :, it's a new path prefix - case "$line" in /*:*) - library_path_prefix=$(echo "$line" | cut -d: -f1) - host_library_paths=$host_library_paths$library_path_prefix: - esac - done < GAMEID= PROTONPATH= ./gamelauncher.sh " + echo "Ex:" + echo "WINEPREFIX=$HOME/Games/epic-games-store GAMEID=egs PROTONPATH=\"$HOME/.steam/steam/compatibilitytools.d/GE-Proton8-28\" ./gamelauncher.sh \"$HOME/Games/epic-games-store/drive_c/Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe\" \"-opengl -SkipBuildPatchPrereq\"" + exit 1 +fi + +me="$(readlink -f "$0")" +here="${me%/*}" + +if [ "$WINEPREFIX" ]; then + if [ ! -d "$WINEPREFIX" ]; then + mkdir -p "$WINEPREFIX" + export PROTON_DLL_COPY="*" + fi + if [ ! -d "$WINEPREFIX"/pfx ]; then + ln -s "$WINEPREFIX" "$WINEPREFIX"/pfx > /dev/null 2>&1 + fi + if [ ! -f "$WINEPREFIX"/tracked_files ]; then + touch "$WINEPREFIX"/tracked_files + fi + if [ ! -f "$WINEPREFIX/dosdevices/" ]; then + mkdir -p "$WINEPREFIX"/dosdevices + ln -s "../drive_c" "$WINEPREFIX/dosdevices/c:" > /dev/null 2>&1 + fi +fi + +if [ -n "$PROTONPATH" ]; then + if [ ! -d "$PROTONPATH" ]; then + echo "ERROR: $PROTONPATH is invalid, aborting!" + exit 1 + fi +fi + +export ULWGL_ID="$GAMEID" +export STEAM_COMPAT_APP_ID="0" +numcheck='^[0-9]+$' +if echo "$ULWGL_ID" | cut -d "-" -f 2 | grep -Eq "$numcheck"; then + STEAM_COMPAT_APP_ID=$(echo "$ULWGL_ID" | cut -d "-" -f 2) + export STEAM_COMPAT_APP_ID +fi +export SteamAppId="$STEAM_COMPAT_APP_ID" +export SteamGameId="$STEAM_COMPAT_APP_ID" + +# TODO: Ideally this should be the main game install path, which is often, but not always the path of the game's executable. +if [ -z "$STEAM_COMPAT_INSTALL_PATH" ]; then + exepath="$(readlink -f "$1")" + gameinstallpath="${exepath%/*}" + export STEAM_COMPAT_INSTALL_PATH="$gameinstallpath" +fi + +compat_lib_path=$(findmnt -T "$STEAM_COMPAT_INSTALL_PATH" | tail -n 1 | awk '{ print $1 }') +if [ "$compat_lib_path" != "/" ]; then + export STEAM_COMPAT_LIBRARY_PATHS="${STEAM_COMPAT_LIBRARY_PATHS:+"${STEAM_COMPAT_LIBRARY_PATHS}:"}$compat_lib_path" +fi + +if [ -z "$STEAM_RUNTIME_LIBRARY_PATH" ]; then + # The following info taken from steam ~/.local/share/ubuntu12_32/steam-runtime/run.sh + host_library_paths= + exit_status=0 + ldconfig_output=$(/sbin/ldconfig -XNv 2> /dev/null; exit $?) || exit_status=$? + if [ $exit_status != 0 ]; then + echo "Warning: An unexpected error occurred while executing \"/sbin/ldconfig -XNv\", the exit status was $exit_status" + fi + + while read -r line; do + # If line starts with a leading / and contains :, it's a new path prefix + case "$line" in /*:*) + library_path_prefix=$(echo "$line" | cut -d: -f1) + host_library_paths=$host_library_paths$library_path_prefix: + esac + done < Dict[str, str]: """Enable Steam Game Drive functionality. Expects STEAM_COMPAT_INSTALL_PATH to be set diff --git a/gamelauncher.py b/ulwgl_run.py similarity index 58% rename from gamelauncher.py rename to ulwgl_run.py index 49fc9da11..c0925f7ce 100755 --- a/gamelauncher.py +++ b/ulwgl_run.py @@ -2,83 +2,102 @@ import os import argparse -from argparse import ArgumentParser, _ArgumentGroup, Namespace +from argparse import ArgumentParser, Namespace import sys from pathlib import Path import tomllib -from typing import Dict, Any, List, Set -import gamelauncher_plugins +from typing import Dict, Any, List, Set, Union, Tuple +import ulwgl_plugins from re import match - -# TODO: Only set the environment variables that are not empty import subprocess -def parse_args() -> Namespace: # noqa: D103 - stores: List[str] = [ - "amazon", - "battlenet", - "ea", - "egs", - "gog", - "humble", - "itchio", - "ubisoft", - ] +def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103 + opt_args: Set[str] = {"--help", "-h", "--config"} exe: str = Path(__file__).name - usage: str = """ + usage: str = f""" example usage: - {} --config example.toml - {} --config /home/foo/example.toml --options '-opengl' - WINEPREFIX= GAMEID= PROTONPATH= {} --exe /home/foo/example.exe --options '-opengl' - WINEPREFIX= GAMEID= PROTONPATH= {} --exe /home/foo/example.exe --store gog - WINEPREFIX= GAMEID= PROTONPATH= {} --exe "" - WINEPREFIX= GAMEID= PROTONPATH= {} --exe /home/foo/example.exe --verb waitforexitandrun - """.format(exe, exe, exe, exe, exe, exe) - + WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe + WINEPREFIX= GAMEID= PROTONPATH= {exe} /home/foo/example.exe -opengl + WINEPREFIX= GAMEID= PROTONPATH= {exe} "" + WINEPREFIX= GAMEID= PROTONPATH= PROTON_VERB= {exe} /home/foo/example.exe + WINEPREFIX= GAMEID= PROTONPATH= STORE= {exe} /home/foo/example.exe + {exe} --config /home/foo/example.toml + """ parser: ArgumentParser = argparse.ArgumentParser( description="Unified Linux Wine Game Launcher", epilog=usage, formatter_class=argparse.RawTextHelpFormatter, ) - group: _ArgumentGroup = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--config", help="path to TOML file") - group.add_argument( - "--exe", - help="path to game executable\npass an empty string to create a prefix", - default=None, - ) - parser.add_argument( - "--verb", - help="a verb to pass to Proton (default: waitforexitandrun)", - ) - parser.add_argument( - "--options", - help="launch options for game executable\nNOTE: options must be wrapped in quotes", - ) - parser.add_argument( - "--store", - help=f"the store of the game executable\nNOTE: will override the store specified in config\nexamples: {stores}", - ) + parser.add_argument("--config", help="path to TOML file") + + if not sys.argv[1:]: + err: str = "Please see project README.md for more info and examples.\nhttps://github.com/Open-Wine-Components/ULWGL-launcher" + parser.print_help() + raise SystemExit(err) - return parser.parse_args(sys.argv[1:]) + if sys.argv[1:][0] in opt_args: + return parser.parse_args(sys.argv[1:]) + return (sys.argv[1], sys.argv[2:]) -def _setup_pfx(path: str) -> None: + +def setup_pfx(path: str) -> None: """Create a symlink to the WINE prefix and tracked_files file.""" if not (Path(path + "/pfx")).expanduser().is_symlink(): # When creating the symlink, we want it to be in expanded form when passed unexpanded paths - # Example: pfx -> /home/.wine + # Example: pfx -> /home/foo/.wine # NOTE: When parsing a config file, an error can be raised if the prefix doesn't already exist Path(path + "/pfx").expanduser().symlink_to(Path(path).expanduser()) Path(path + "/tracked_files").expanduser().touch() -def check_env(env: Dict[str, str]) -> Dict[str, str]: +def check_env( + env: Dict[str, str], toml: Dict[str, Any] = None +) -> Union[Dict[str, str], Dict[str, Any]]: """Before executing a game, check for environment variables and set them. WINEPREFIX, GAMEID and PROTONPATH are strictly required. """ + if toml: + # Check for required or empty key/value pairs when reading a TOML config + # NOTE: Casing matters in the config and we don't check if the game id is set + table: str = "ulwgl" + required_keys: List[str] = ["proton", "prefix", "exe"] + + if table not in toml: + err: str = f"Table '{table}' in TOML is not defined." + raise ValueError(err) + + for key in required_keys: + if key not in toml[table]: + err: str = f"The following key in table '{table}' is required: {key}" + raise ValueError(err) + + # Raise an error for executables that do not exist + # One case this can happen is when game options are appended at the end of the exe + # Users should use launch_args for that + if key == "exe" and not Path(toml[table][key]).expanduser().is_file(): + val: str = toml[table][key] + err: str = f"Value for key '{key}' in TOML is not a file: {val}" + raise FileNotFoundError(err) + + # The proton and wine prefix need to be folders + if ( + key == "proton" and not Path(toml[table][key]).expanduser().is_dir() + ) or (key == "prefix" and not Path(toml[table][key]).expanduser().is_dir()): + dir: str = Path(toml[table][key]).expanduser().as_posix() + err: str = f"Value for key '{key}' in TOML is not a directory: {dir}" + raise NotADirectoryError(err) + + # Check for empty keys + for key, val in toml[table].items(): + if not val and isinstance(val, str): + err: str = f"Value is empty for '{key}' in TOML.\nPlease specify a value or remove the following entry:\n{key} = {val}" + raise ValueError(err) + + return toml + if "WINEPREFIX" not in os.environ: err: str = "Environment variable not set or not a directory: WINEPREFIX" raise ValueError(err) @@ -99,30 +118,68 @@ def check_env(env: Dict[str, str]) -> Dict[str, str]: err: str = "Environment variable not set or not a directory: PROTONPATH" raise ValueError(err) env["PROTONPATH"] = os.environ["PROTONPATH"] - env["STEAM_COMPAT_INSTALL_PATH"] = os.environ["PROTONPATH"] return env -def set_env(env: Dict[str, str], args: Namespace) -> Dict[str, str]: +def set_env( + env: Dict[str, str], args: Union[Namespace, Tuple[str, List[str]]] +) -> Dict[str, str]: """Set various environment variables for the Steam RT. - Expects to be invoked if not reading a TOML file + Filesystem paths will be formatted and expanded as POSIX """ - _setup_pfx(env["WINEPREFIX"]) - is_create_prefix: bool = False + verbs: Set[str] = { + "waitforexitandrun", + "run", + "runinprefix", + "destroyprefix", + "getcompatpath", + "getnativepath", + } + + # PROTON_VERB + # For invalid Proton verbs, just assign the waitforexitandrun + if "PROTON_VERB" in os.environ and os.environ["PROTON_VERB"] in verbs: + env["PROTON_VERB"] = os.environ["PROTON_VERB"] + else: + env["PROTON_VERB"] = "waitforexitandrun" + + # EXE + # Empty string for EXE will be used to create a prefix + if isinstance(args, tuple) and isinstance(args[0], str) and not args[0]: + env["EXE"] = "" + env["STEAM_COMPAT_INSTALL_PATH"] = "" + env["PROTON_VERB"] = "waitforexitandrun" + elif isinstance(args, tuple): + env["EXE"] = Path(args[0]).expanduser().as_posix() + env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix() + else: + # Config branch + env["EXE"] = Path(env["EXE"]).expanduser().as_posix() + env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.as_posix() + + if "STORE" in os.environ: + env["STORE"] = os.environ["STORE"] - if not getattr(args, "exe", None): - is_create_prefix = True + # ULWGL_ID + env["ULWGL_ID"] = env["GAMEID"] + env["STEAM_COMPAT_APP_ID"] = "0" - # Sets the environment variables: EXE - for arg, val in vars(args).items(): - if arg == "exe" and not is_create_prefix: - # NOTE: options can possibly be appended at the end - env["EXE"] = val - elif arg == "options" and val and not is_create_prefix: - # NOTE: assume it's space separated - env["EXE"] = env["EXE"] + " " + " ".join(val.split(" ")) + if match(r"^ulwgl-[\d\w]+$", env["ULWGL_ID"]): + env["STEAM_COMPAT_APP_ID"] = env["ULWGL_ID"][env["ULWGL_ID"].find("-") + 1 :] + env["SteamAppId"] = env["STEAM_COMPAT_APP_ID"] + env["SteamGameId"] = env["SteamAppId"] + + # PATHS + env["WINEPREFIX"] = Path(env["WINEPREFIX"]).expanduser().as_posix() + env["PROTONPATH"] = Path(env["PROTONPATH"]).expanduser().as_posix() + env["STEAM_COMPAT_DATA_PATH"] = env["WINEPREFIX"] + env["STEAM_COMPAT_SHADER_PATH"] = env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" + env["STEAM_COMPAT_TOOL_PATHS"] = ( + env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() + ) + env["STEAM_COMPAT_MOUNTS"] = env["STEAM_COMPAT_TOOL_PATHS"] return env @@ -149,62 +206,35 @@ def set_env_toml(env: Dict[str, str], args: Namespace) -> Dict[str, str]: with Path(path_config).open(mode="rb") as file: toml = tomllib.load(file) - if not ( - Path(toml["ulwgl"]["prefix"]).expanduser().is_dir() - or Path(toml["ulwgl"]["proton"]).expanduser().is_dir() - ): - err: str = "Value for 'prefix' or 'proton' in TOML is not a directory." - raise NotADirectoryError(err) + check_env(env, toml) - # Set the values read from TOML to environment variables - # If necessary, raise an error on invalid inputs for key, val in toml["ulwgl"].items(): - # Handle cases for empty values - if not val and isinstance(val, str): - err: str = f'Value is empty for key in TOML: {key}\nPlease specify a value or remove the following entry:\n{key} = "{val}"' - raise ValueError(err) if key == "prefix": env["WINEPREFIX"] = val - _setup_pfx(val) elif key == "game_id": env["GAMEID"] = val elif key == "proton": env["PROTONPATH"] = val - env["STEAM_COMPAT_INSTALL_PATH"] = val elif key == "store": env["STORE"] = val elif key == "exe": - # Raise an error for executables that do not exist - # One case this can happen is when game options are appended at the end of the exe - if not Path(val).expanduser().is_file(): - err: str = "Value for key 'exe' in TOML is not a file." - raise FileNotFoundError(err) - - # It's possible for users to pass values to --options - # Add any if they exist if toml.get("ulwgl").get("launch_args"): env["EXE"] = val + " " + " ".join(toml.get("ulwgl").get("launch_args")) else: env["EXE"] = val - - if getattr(args, "options", None): - # Assume space separated options and just trust it - env["EXE"] = ( - env["EXE"] - + " " - + " ".join(getattr(args, "options", None).split(" ")) - ) - return env -def build_command(env: Dict[str, str], command: List[str], verb: str) -> List[str]: +def build_command( + env: Dict[str, str], command: List[str], opts: List[str] = None +) -> List[str]: """Build the command to be executed.""" paths: List[Path] = [ Path(Path().home().as_posix() + "/.local/share/ULWGL/ULWGL"), Path(Path(__file__).cwd().as_posix() + "/ULWGL"), ] entry_point: str = "" + verb: str = env["PROTON_VERB"] # Find the ULWGL script in $HOME/.local/share then cwd for path in paths: @@ -230,6 +260,9 @@ def build_command(env: Dict[str, str], command: List[str], verb: str) -> List[st [Path(env.get("PROTONPATH") + "/proton").as_posix(), verb, env.get("EXE")] ) + if opts: + command.extend([*opts]) + return command @@ -253,68 +286,32 @@ def main() -> None: # noqa: D103 "SteamGameId": "", "STEAM_RUNTIME_LIBRARY_PATH": "", "STORE": "", + "PROTON_VERB": "", + "ULWGL_ID": "", } command: List[str] = [] - verb: str = "waitforexitandrun" - # Represents a valid list of current supported Proton verbs - verbs: Set[str] = { - "waitforexitandrun", - "run", - "runinprefix", - "destroyprefix", - "getcompatpath", - "getnativepath", - } - args: Namespace = parse_args() + args: Union[Namespace, Tuple[str, List[str]]] = parse_args() + opts: List[str] = None - if getattr(args, "config", None): + if isinstance(args, Namespace): set_env_toml(env, args) else: + # Reference the game options + opts = args[1] check_env(env) - set_env(env, args) - - if getattr(args, "verb", None) and getattr(args, "verb", None) in verbs: - verb = getattr(args, "verb", None) - - if getattr(args, "store", None): - env["STORE"] = getattr(args, "store", None) - - env["ULWGL_ID"] = env["GAMEID"] - env["STEAM_COMPAT_APP_ID"] = "0" - - if match(r"^ulwgl-[\d\w]+$", env["ULWGL_ID"]): - env["STEAM_COMPAT_APP_ID"] = env["ULWGL_ID"][env["ULWGL_ID"].find("-") + 1 :] - - env["SteamAppId"] = env["STEAM_COMPAT_APP_ID"] - env["SteamGameId"] = env["SteamAppId"] - env["WINEPREFIX"] = Path(env["WINEPREFIX"]).expanduser().as_posix() - env["PROTONPATH"] = Path(env["PROTONPATH"]).expanduser().as_posix() - env["STEAM_COMPAT_DATA_PATH"] = env["WINEPREFIX"] - env["STEAM_COMPAT_SHADER_PATH"] = env["STEAM_COMPAT_DATA_PATH"] + "/shadercache" - env["STEAM_COMPAT_INSTALL_PATH"] = Path(env["EXE"]).parent.expanduser().as_posix() - env["EXE"] = Path(env["EXE"]).expanduser().as_posix() - env["STEAM_COMPAT_TOOL_PATHS"] = ( - env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix() - ) - env["STEAM_COMPAT_MOUNTS"] = env["STEAM_COMPAT_TOOL_PATHS"] - # Create an empty Proton prefix when asked - if not getattr(args, "exe", None) and not getattr(args, "config", None): - env["EXE"] = "" - env["STEAM_COMPAT_INSTALL_PATH"] = "" - verb = "waitforexitandrun" + setup_pfx(env["WINEPREFIX"]) + set_env(env, args) - # Game Drive functionality - gamelauncher_plugins.enable_steam_game_drive(env) + # Game Drive + ulwgl_plugins.enable_steam_game_drive(env) - # Set all environment variable + # Set all environment variables # NOTE: `env` after this block should be read only for key, val in env.items(): - print(f"Setting environment variable: {key}={val}") os.environ[key] = val - build_command(env, command, verb) - print(f"The following command will be executed: {command}") + build_command(env, command, opts) subprocess.run(command, check=True, stdout=subprocess.PIPE, text=True) diff --git a/ulwgl_test.py b/ulwgl_test.py new file mode 100644 index 000000000..496934405 --- /dev/null +++ b/ulwgl_test.py @@ -0,0 +1,1080 @@ +import unittest +import ulwgl_run +import os +import argparse +from argparse import Namespace +from unittest.mock import patch +from pathlib import Path +from tomllib import TOMLDecodeError +from shutil import rmtree +import re +import ulwgl_plugins + + +class TestGameLauncher(unittest.TestCase): + """Test suite for ulwgl_run.py. + + TODO: test for mutually exclusive options + """ + + def setUp(self): + """Create the test directory, exe and environment variables.""" + self.env = { + "WINEPREFIX": "", + "GAMEID": "", + "PROTON_CRASH_REPORT_DIR": "/tmp/ULWGL_crashreports", + "PROTONPATH": "", + "STEAM_COMPAT_APP_ID": "", + "STEAM_COMPAT_TOOL_PATHS": "", + "STEAM_COMPAT_LIBRARY_PATHS": "", + "STEAM_COMPAT_MOUNTS": "", + "STEAM_COMPAT_INSTALL_PATH": "", + "STEAM_COMPAT_CLIENT_INSTALL_PATH": "", + "STEAM_COMPAT_DATA_PATH": "", + "STEAM_COMPAT_SHADER_PATH": "", + "FONTCONFIG_PATH": "", + "EXE": "", + "SteamAppId": "", + "SteamGameId": "", + "STEAM_RUNTIME_LIBRARY_PATH": "", + "ULWGL_ID": "", + "STORE": "", + "PROTON_VERB": "", + } + self.test_opts = "-foo -bar" + # Proton verb + # Used when testing build_command + self.test_verb = "waitforexitandrun" + # Test directory + self.test_file = "./tmp.WMYQiPb9A" + # Executable + self.test_exe = self.test_file + "/" + "foo" + Path(self.test_file).mkdir(exist_ok=True) + Path(self.test_exe).touch() + + def tearDown(self): + """Unset environment variables and delete test files after each test.""" + for key, val in self.env.items(): + if key in os.environ: + os.environ.pop(key) + + if Path(self.test_file).exists(): + rmtree(self.test_file) + + def test_game_drive_empty(self): + """Test enable_steam_game_drive. + + Empty WINE prefixes can be created by passing an empty string to --exe + During this process, we attempt to prepare setting up game drive and set the values for STEAM_RUNTIME_LIBRARY_PATH and STEAM_COMPAT_INSTALL_PATHS + The resulting value of those variables should be colon delimited string with no leading colons and contain only /usr/lib or /usr/lib32 + """ + args = None + result_gamedrive = None + Path(self.test_file + "/proton").touch() + + # Replicate main's execution and test up until enable_steam_game_drive + with patch("sys.argv", ["", ""]): + os.environ["WINEPREFIX"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + os.environ["GAMEID"] = self.test_file + os.environ["STORE"] = self.test_file + # Args + args = ulwgl_run.parse_args() + # Config + ulwgl_run.check_env(self.env) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + ulwgl_run.set_env(self.env, args) + # Game drive + result_gamedrive = ulwgl_plugins.enable_steam_game_drive(self.env) + + for key, val in self.env.items(): + os.environ[key] = val + + # Game drive + self.assertTrue(result_gamedrive is self.env, "Expected the same reference") + self.assertTrue( + self.env["STEAM_RUNTIME_LIBRARY_PATH"], + "Expected two elements in STEAM_RUNTIME_LIBRARY_PATHS", + ) + + # We just expect /usr/lib and /usr/lib32 + self.assertEqual( + len(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":")), + 2, + "Expected two values in STEAM_RUNTIME_LIBRARY_PATH", + ) + + # We need to sort the elements because the values were originally in a set + str1, str2 = [*sorted(self.env["STEAM_RUNTIME_LIBRARY_PATH"].split(":"))] + + # Check that there are no trailing colons or unexpected characters + self.assertEqual(str1, "/usr/lib", "Expected /usr/lib") + self.assertEqual(str2, "/usr/lib32", "Expected /usr/lib32") + + # Both of these values should be empty still after calling enable_steam_game_drive + self.assertFalse( + self.env["STEAM_COMPAT_INSTALL_PATH"], + "Expected STEAM_COMPAT_INSTALL_PATH to be empty when passing an empty EXE", + ) + self.assertFalse(self.env["EXE"], "Expected EXE to be empty on empty string") + + def test_build_command_nofile(self): + """Test build_command. + + A FileNotFoundError should be raised if $PROTONPATH/proton does not exist + NOTE: Also, FileNotFoundError will be raised if the _v2-entry-point (ULWGL) is not in $HOME/.local/share/ULWGL or in cwd + """ + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}"] + exe = "{self.test_exe}" + """ + toml_path = self.test_file + "/" + test_toml + result = None + test_command = [] + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + # Config + ulwgl_run.set_env_toml(self.env, result) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + ulwgl_run.set_env(self.env, result) + # Game drive + ulwgl_plugins.enable_steam_game_drive(self.env) + + for key, val in self.env.items(): + os.environ[key] = val + + # Build + with self.assertRaisesRegex(FileNotFoundError, "proton"): + ulwgl_run.build_command(self.env, test_command) + + def test_build_command_toml(self): + """Test build_command. + + After parsing a valid TOML file, be sure we do not raise a FileNotFoundError + """ + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + exe = "{self.test_exe}" + """ + toml_path = self.test_file + "/" + test_toml + result = None + test_command = [] + test_command_result = None + + Path(self.test_file + "/proton").touch() + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + # Config + ulwgl_run.set_env_toml(self.env, result) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + ulwgl_run.set_env(self.env, result) + # Game drive + ulwgl_plugins.enable_steam_game_drive(self.env) + + for key, val in self.env.items(): + os.environ[key] = val + + # Build + test_command_result = ulwgl_run.build_command(self.env, test_command) + self.assertTrue( + test_command_result is test_command, "Expected the same reference" + ) + + # Verify contents of the command + entry_point, opt1, verb, opt2, proton, verb2, exe = [*test_command] + # The entry point dest could change. Just check if there's a value + self.assertTrue(entry_point, "Expected an entry point") + self.assertEqual(opt1, "--verb", "Expected --verb") + self.assertEqual(verb, self.test_verb, "Expected a verb") + self.assertEqual(opt2, "--", "Expected --") + self.assertEqual( + proton, + Path(self.env.get("PROTONPATH") + "/proton").as_posix(), + "Expected the proton file", + ) + self.assertEqual(verb2, self.test_verb, "Expected a verb") + self.assertEqual(exe, self.env["EXE"], "Expected the EXE") + + def test_build_command(self): + """Test build_command. + + After parsing valid environment variables set by the user, be sure we do not raise a FileNotFoundError + NOTE: Also, FileNotFoundError will be raised if the _v2-entry-point (ULWGL) is not in $HOME/.local/share/ULWGL or in cwd + """ + result_args = None + test_command = [] + + # Mock the /proton file + Path(self.test_file + "/proton").touch() + + with patch("sys.argv", ["", self.test_exe]): + os.environ["WINEPREFIX"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + os.environ["GAMEID"] = self.test_file + os.environ["STORE"] = self.test_file + # Args + result_args = ulwgl_run.parse_args() + # Config + ulwgl_run.check_env(self.env) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + ulwgl_run.set_env(self.env, result_args) + # Game drive + ulwgl_plugins.enable_steam_game_drive(self.env) + + for key, val in self.env.items(): + os.environ[key] = val + + # Build + test_command = ulwgl_run.build_command(self.env, test_command) + self.assertIsInstance(test_command, list, "Expected a List from build_command") + self.assertEqual( + len(test_command), 7, "Expected 7 elements in the list from build_command" + ) + # Verify contents + entry_point, opt1, verb, opt2, proton, verb2, exe = [*test_command] + # The entry point dest could change. Just check if there's a value + self.assertTrue(entry_point, "Expected an entry point") + self.assertEqual(opt1, "--verb", "Expected --verb") + self.assertEqual(verb, self.test_verb, "Expected a verb") + self.assertEqual(opt2, "--", "Expected --") + self.assertEqual( + proton, + Path(self.env.get("PROTONPATH") + "/proton").as_posix(), + "Expected the proton file", + ) + self.assertEqual(verb2, self.test_verb, "Expected a verb") + self.assertEqual(exe, self.env["EXE"], "Expected the EXE") + + def test_set_env_toml_config(self): + """Test set_env_toml when passing a configuration file. + + An FileNotFoundError should be raised when passing a TOML file that doesn't exist + """ + test_file = "foo.toml" + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=test_file), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + with self.assertRaisesRegex(FileNotFoundError, test_file): + ulwgl_run.set_env_toml(self.env, result) + + def test_set_env_toml_opts_nofile(self): + """Test set_env_toml for options that are a file. + + An error should not be raised if a launch argument is a file + We allow this behavior to give users flexibility at the cost of security + """ + test_toml = "foo.toml" + toml_path = self.test_file + "/" + test_toml + toml_str = f""" + [ulwgl] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{toml_path}"] + exe = "{self.test_exe}" + """ + result = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + ulwgl_run.set_env_toml(self.env, result) + + # Check if its the TOML file we just created + self.assertTrue( + Path(self.env["EXE"].split(" ")[1]).is_file(), + "Expected a file to be appended to the executable", + ) + + def test_set_env_toml_nofile(self): + """Test set_env_toml for values that are not a file. + + A FileNotFoundError should be raised if the 'exe' is not a file + """ + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + exe = "./bar" + """ + toml_path = self.test_file + "/" + test_toml + result = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + with self.assertRaisesRegex(FileNotFoundError, "exe"): + ulwgl_run.set_env_toml(self.env, result) + + def test_set_env_toml_err(self): + """Test set_env_toml for valid TOML. + + A TOMLDecodeError should be raised for invalid values + """ + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = [[ + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + """ + toml_path = self.test_file + "/" + test_toml + result = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + # Env + with self.assertRaisesRegex(TOMLDecodeError, "Invalid"): + ulwgl_run.set_env_toml(self.env, result) + + def test_set_env_toml_nodir(self): + """Test set_env_toml if certain key/value are not a dir. + + An IsDirectoryError should be raised if the following keys are not dir: proton, prefix + """ + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = "foo" + proton = "foo" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + """ + toml_path = self.test_file + "/" + test_toml + result = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + with self.assertRaisesRegex(NotADirectoryError, "proton"): + ulwgl_run.set_env_toml(self.env, result) + + def test_set_env_toml_tables(self): + """Test set_env_toml for expected tables. + + A ValueError should be raised if the following tables are absent: ulwgl + """ + test_toml = "foo.toml" + toml_str = f""" + [foo] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + """ + toml_path = self.test_file + "/" + test_toml + result = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + with self.assertRaisesRegex(ValueError, "ulwgl"): + ulwgl_run.set_env_toml(self.env, result) + + def test_set_env_toml_paths(self): + """Test set_env_toml when specifying unexpanded file path values in the config file. + + Example: ~/Games/foo.exe + An error should not be raised when passing unexpanded paths to the config file as well as the prefix, proton and exe keys + """ + test_toml = "foo.toml" + pattern = r"^/home/[a-zA-Z]+" + + # Replaces the expanded path to unexpanded + # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file + path_to_tmp = Path( + Path(__file__).cwd().as_posix() + "/" + self.test_file + ).as_posix() + path_to_exe = Path( + Path(__file__).cwd().as_posix() + "/" + self.test_exe + ).as_posix() + + # Replace /home/[a-zA-Z]+ substring in path with tilda + unexpanded_path = re.sub( + pattern, + "~", + path_to_tmp, + ) + unexpanded_exe = re.sub( + pattern, + "~", + path_to_exe, + ) + toml_str = f""" + [ulwgl] + prefix = "{unexpanded_path}" + proton = "{unexpanded_path}" + game_id = "{unexpanded_path}" + exe = "{unexpanded_exe}" + """ + # Path to TOML in unexpanded form + toml_path = unexpanded_path + "/" + test_toml + result = None + result_set_env = None + + Path(toml_path).expanduser().touch() + + with Path(toml_path).expanduser().open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + result_set_env = ulwgl_run.set_env_toml(self.env, result) + self.assertTrue(result_set_env is self.env, "Expected the same reference") + + # Check that the paths are still in the unexpanded form after setting the env + # In main, we only expand them after this function exits to prepare for building the command + self.assertEqual( + self.env["EXE"], unexpanded_exe, "Expected path not to be expanded" + ) + self.assertEqual( + self.env["PROTONPATH"], + unexpanded_path, + "Expected path not to be expanded", + ) + self.assertEqual( + self.env["WINEPREFIX"], + unexpanded_path, + "Expected path not to be expanded", + ) + self.assertEqual( + self.env["GAMEID"], unexpanded_path, "Expectd path not to be expanded" + ) + + def test_set_env_toml(self): + """Test set_env_toml.""" + test_toml = "foo.toml" + toml_str = f""" + [ulwgl] + prefix = "{self.test_file}" + proton = "{self.test_file}" + game_id = "{self.test_file}" + launch_args = ["{self.test_file}", "{self.test_file}"] + exe = "{self.test_exe}" + """ + toml_path = self.test_file + "/" + test_toml + result = None + result_set_env = None + + Path(toml_path).touch() + + with Path(toml_path).open(mode="w") as file: + file.write(toml_str) + + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=toml_path), + ): + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + self.assertTrue(vars(result).get("config"), "Expected a value for --config") + # Env + result_set_env = ulwgl_run.set_env_toml(self.env, result) + self.assertTrue(result_set_env is self.env, "Expected the same reference") + self.assertTrue(self.env["EXE"], "Expected EXE to be set") + self.assertEqual( + self.env["EXE"], + self.test_exe + " " + " ".join([self.test_file, self.test_file]), + "Expectd GAMEID to be set", + ) + self.assertEqual( + self.env["PROTONPATH"], + self.test_file, + "Expected PROTONPATH to be set", + ) + self.assertEqual( + self.env["WINEPREFIX"], + self.test_file, + "Expected WINEPREFIX to be set", + ) + self.assertEqual( + self.env["GAMEID"], self.test_file, "Expectd GAMEID to be set" + ) + + def test_set_env_opts(self): + """Test set_env. + + Ensure no failures and verify that an option is passed to the executable + """ + result = None + test_str = "foo" + + # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= STORE= PROTON_VERB= ulwgl_run foo.exe -foo + with patch("sys.argv", ["", self.test_exe, test_str]): + os.environ["WINEPREFIX"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + os.environ["GAMEID"] = test_str + os.environ["STORE"] = test_str + os.environ["PROTON_VERB"] = self.test_verb + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance(result, tuple, "Expected a tuple") + self.assertIsInstance(result[0], str, "Expected a string") + self.assertIsInstance(result[1], list, "Expected a list as options") + self.assertEqual( + result[0], "./tmp.WMYQiPb9A/foo", "Expected EXE to be unexpanded" + ) + self.assertEqual( + *result[1], + test_str, + "Expected the test string when passed as an option", + ) + # Check + ulwgl_run.check_env(self.env) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + result = ulwgl_run.set_env(self.env, result[0:]) + self.assertTrue(result is self.env, "Expected the same reference") + + path_exe = Path(self.test_exe).expanduser().as_posix() + path_file = Path(self.test_file).expanduser().as_posix() + + # After calling set_env all paths should be expanded POSIX form + self.assertEqual(self.env["EXE"], path_exe, "Expected EXE to be expanded") + self.assertEqual(self.env["STORE"], test_str, "Expected STORE to be set") + self.assertEqual( + self.env["PROTONPATH"], path_file, "Expected PROTONPATH to be set" + ) + self.assertEqual( + self.env["WINEPREFIX"], path_file, "Expected WINEPREFIX to be set" + ) + self.assertEqual(self.env["GAMEID"], test_str, "Expected GAMEID to be set") + self.assertEqual( + self.env["PROTON_VERB"], + self.test_verb, + "Expected PROTON_VERB to be set", + ) + + def test_set_env_id(self): + """Test set_env. + + Verify that environment variables (dictionary) are set after calling set_env when passing a valid ULWGL_ID + When a valid ULWGL_ID is set, the STEAM_COMPAT_APP_ID variables should be the stripped ULWGL_ID + """ + result = None + test_str = "foo" + ulwgl_id = "ulwgl-271590" + + # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= STORE= PROTON_VERB= ulwgl_run foo.exe + with patch("sys.argv", ["", self.test_exe]): + os.environ["WINEPREFIX"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + os.environ["GAMEID"] = ulwgl_id + os.environ["STORE"] = test_str + os.environ["PROTON_VERB"] = self.test_verb + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance(result, tuple, "Expected a tuple") + self.assertIsInstance(result[0], str, "Expected a string") + self.assertIsInstance(result[1], list, "Expected a list as options") + self.assertEqual( + result[0], "./tmp.WMYQiPb9A/foo", "Expected EXE to be unexpanded" + ) + self.assertFalse( + result[1], "Expected an empty list when passing no options" + ) + # Check + ulwgl_run.check_env(self.env) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + result = ulwgl_run.set_env(self.env, result[0:]) + self.assertTrue(result is self.env, "Expected the same reference") + + path_exe = Path(self.test_exe).expanduser().as_posix() + path_file = Path(self.test_file).expanduser().as_posix() + + # After calling set_env all paths should be expanded POSIX form + self.assertEqual(self.env["EXE"], path_exe, "Expected EXE to be expanded") + self.assertEqual(self.env["STORE"], test_str, "Expected STORE to be set") + self.assertEqual( + self.env["PROTONPATH"], path_file, "Expected PROTONPATH to be set" + ) + self.assertEqual( + self.env["WINEPREFIX"], path_file, "Expected WINEPREFIX to be set" + ) + self.assertEqual(self.env["GAMEID"], ulwgl_id, "Expected GAMEID to be set") + self.assertEqual( + self.env["PROTON_VERB"], + self.test_verb, + "Expected PROTON_VERB to be set", + ) + # ULWGL + self.assertEqual( + self.env["ULWGL_ID"], + self.env["GAMEID"], + "Expected ULWGL_ID to be GAMEID", + ) + self.assertEqual(self.env["ULWGL_ID"], ulwgl_id, "Expected ULWGL_ID") + # Should be stripped -- everything after the hyphen + self.assertEqual( + self.env["STEAM_COMPAT_APP_ID"], + ulwgl_id[ulwgl_id.find("-") + 1 :], + "Expected STEAM_COMPAT_APP_ID to be the stripped ULWGL_ID", + ) + self.assertEqual( + self.env["SteamAppId"], + self.env["STEAM_COMPAT_APP_ID"], + "Expected SteamAppId to be STEAM_COMPAT_APP_ID", + ) + self.assertEqual( + self.env["SteamGameId"], + self.env["SteamAppId"], + "Expected SteamGameId to be STEAM_COMPAT_APP_ID", + ) + + # PATHS + self.assertEqual( + self.env["STEAM_COMPAT_SHADER_PATH"], + self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache", + "Expected STEAM_COMPAT_SHADER_PATH to be set", + ) + self.assertEqual( + self.env["STEAM_COMPAT_TOOL_PATHS"], + self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix(), + "Expected STEAM_COMPAT_TOOL_PATHS to be set", + ) + self.assertEqual( + self.env["STEAM_COMPAT_MOUNTS"], + self.env["STEAM_COMPAT_TOOL_PATHS"], + "Expected STEAM_COMPAT_MOUNTS to be set", + ) + + def test_set_env(self): + """Test set_env. + + Verify that environment variables (dictionary) are set after calling set_env + """ + result = None + test_str = "foo" + + # Replicate the usage WINEPREFIX= PROTONPATH= GAMEID= STORE= PROTON_VERB= ulwgl_run foo.exe + with patch("sys.argv", ["", self.test_exe]): + os.environ["WINEPREFIX"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + os.environ["GAMEID"] = test_str + os.environ["STORE"] = test_str + os.environ["PROTON_VERB"] = self.test_verb + # Args + result = ulwgl_run.parse_args() + self.assertIsInstance(result, tuple, "Expected a tuple") + self.assertIsInstance(result[0], str, "Expected a string") + self.assertIsInstance(result[1], list, "Expected a list as options") + self.assertEqual( + result[0], "./tmp.WMYQiPb9A/foo", "Expected EXE to be unexpanded" + ) + self.assertFalse( + result[1], "Expected an empty list when passing no options" + ) + # Check + ulwgl_run.check_env(self.env) + # Prefix + ulwgl_run.setup_pfx(self.env["WINEPREFIX"]) + # Env + result = ulwgl_run.set_env(self.env, result[0:]) + self.assertTrue(result is self.env, "Expected the same reference") + + path_exe = Path(self.test_exe).expanduser().as_posix() + path_file = Path(self.test_file).expanduser().as_posix() + + # After calling set_env all paths should be expanded POSIX form + self.assertEqual(self.env["EXE"], path_exe, "Expected EXE to be expanded") + self.assertEqual(self.env["STORE"], test_str, "Expected STORE to be set") + self.assertEqual( + self.env["PROTONPATH"], path_file, "Expected PROTONPATH to be set" + ) + self.assertEqual( + self.env["WINEPREFIX"], path_file, "Expected WINEPREFIX to be set" + ) + self.assertEqual(self.env["GAMEID"], test_str, "Expected GAMEID to be set") + self.assertEqual( + self.env["PROTON_VERB"], + self.test_verb, + "Expected PROTON_VERB to be set", + ) + # ULWGL + self.assertEqual( + self.env["ULWGL_ID"], + self.env["GAMEID"], + "Expected ULWGL_ID to be GAMEID", + ) + self.assertEqual( + self.env["STEAM_COMPAT_APP_ID"], + "0", + "Expected STEAM_COMPAT_APP_ID to be 0", + ) + self.assertEqual( + self.env["SteamAppId"], + self.env["STEAM_COMPAT_APP_ID"], + "Expected SteamAppId to be STEAM_COMPAT_APP_ID", + ) + self.assertEqual( + self.env["SteamGameId"], + self.env["SteamAppId"], + "Expected SteamGameId to be STEAM_COMPAT_APP_ID", + ) + + # PATHS + self.assertEqual( + self.env["STEAM_COMPAT_SHADER_PATH"], + self.env["STEAM_COMPAT_DATA_PATH"] + "/shadercache", + "Expected STEAM_COMPAT_SHADER_PATH to be set", + ) + self.assertEqual( + self.env["STEAM_COMPAT_TOOL_PATHS"], + self.env["PROTONPATH"] + ":" + Path(__file__).parent.as_posix(), + "Expected STEAM_COMPAT_TOOL_PATHS to be set", + ) + self.assertEqual( + self.env["STEAM_COMPAT_MOUNTS"], + self.env["STEAM_COMPAT_TOOL_PATHS"], + "Expected STEAM_COMPAT_MOUNTS to be set", + ) + + def test_setup_pfx_symlinks(self): + """Test _setup_pfx for valid symlinks. + + Ensure that symbolic links to the WINE prefix (pfx) are always in expanded form when passed an unexpanded path. + For example: + if WINEPREFIX is /home/foo/.wine + pfx -> /home/foo/.wine + + We do not want the symbolic link such as: + pfx -> ~/.wine + """ + result = None + pattern = r"^/home/[a-zA-Z]+" + unexpanded_path = re.sub( + pattern, + "~", + Path( + Path(self.test_file).cwd().as_posix() + "/" + self.test_file + ).as_posix(), + ) + result = ulwgl_run.setup_pfx(unexpanded_path) + + # Replaces the expanded path to unexpanded + # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + self.assertTrue( + Path(self.test_file + "/tracked_files").is_file(), + "Expected tracked_files to be a file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + + # Check if the symlink is in its unexpanded form + self.assertEqual( + Path(self.test_file + "/pfx").readlink().as_posix(), + Path(unexpanded_path).expanduser().as_posix(), + ) + + def test_setup_pfx_paths(self): + """Test setup_pfx on unexpanded paths. + + An error should not be raised when passing paths such as ~/path/to/prefix. + """ + result = None + pattern = r"^/home/[a-zA-Z]+" + unexpanded_path = re.sub( + pattern, + "~", + Path(Path(self.test_file).as_posix()).as_posix(), + ) + result = ulwgl_run.setup_pfx(unexpanded_path) + + # Replaces the expanded path to unexpanded + # Example: ~/some/path/to/this/file -> /home/foo/path/to/this/file + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + self.assertTrue( + Path(self.test_file + "/tracked_files").is_file(), + "Expected tracked_files to be a file", + ) + + def test_setup_pfx(self): + """Test setup_pfx.""" + result = None + result = ulwgl_run.setup_pfx(self.test_file) + self.assertIsNone( + result, + "Expected None when creating symbolic link to WINE prefix and tracked_files file", + ) + self.assertTrue( + Path(self.test_file + "/pfx").is_symlink(), "Expected pfx to be a symlink" + ) + self.assertTrue( + Path(self.test_file + "/tracked_files").is_file(), + "Expected tracked_files to be a file", + ) + + def test_parse_args(self): + """Test parse_args with no options. + + There's a requirement to create an empty prefix + A SystemExit should be raised in this case: + ./ulwgl_run.py + """ + with self.assertRaises(SystemExit): + ulwgl_run.parse_args() + + def test_parse_args_config(self): + """Test parse_args --config.""" + with patch.object( + ulwgl_run, + "parse_args", + return_value=argparse.Namespace(config=self.test_file), + ): + result = ulwgl_run.parse_args() + self.assertIsInstance( + result, Namespace, "Expected a Namespace from parse_arg" + ) + + def test_env_proton_dir(self): + """Test check_env when $PROTONPATH is not a directory. + + An ValueError should occur if the value is not a directory + """ + with self.assertRaisesRegex(ValueError, "PROTONPATH"): + os.environ["WINEPREFIX"] = self.test_file + os.environ["GAMEID"] = self.test_file + os.environ["PROTONPATH"] = "./foo" + ulwgl_run.check_env(self.env) + self.assertFalse( + Path(os.environ["PROTONPATH"]).is_dir(), + "Expected PROTONPATH to not be a directory", + ) + + def test_env_wine_dir(self): + """Test check_env when $WINEPREFIX is not a directory. + + An error should not be raised if a WINEPREFIX is set but the path has not been created. + """ + os.environ["WINEPREFIX"] = "./foo" + os.environ["GAMEID"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + ulwgl_run.check_env(self.env) + self.assertEqual( + Path(os.environ["WINEPREFIX"]).is_dir(), + True, + "Expected WINEPREFIX to be created if not already exist", + ) + if Path(os.environ["WINEPREFIX"]).is_dir(): + rmtree(os.environ["WINEPREFIX"]) + + def test_env_vars_paths(self): + """Test check_env when setting unexpanded paths for $WINEPREFIX and $PROTONPATH.""" + pattern = r"^/home/[a-zA-Z]+" + path_to_tmp = Path( + Path(__file__).cwd().as_posix() + "/" + self.test_file + ).as_posix() + + # Replace /home/[a-zA-Z]+ substring in path with tilda + unexpanded_path = re.sub( + pattern, + "~", + path_to_tmp, + ) + + result = None + os.environ["WINEPREFIX"] = unexpanded_path + os.environ["GAMEID"] = self.test_file + os.environ["PROTONPATH"] = unexpanded_path + result = ulwgl_run.check_env(self.env) + self.assertTrue(result is self.env, "Expected the same reference") + self.assertEqual( + self.env["WINEPREFIX"], unexpanded_path, "Expected WINEPREFIX to be set" + ) + self.assertEqual( + self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" + ) + self.assertEqual( + self.env["PROTONPATH"], unexpanded_path, "Expected PROTONPATH to be set" + ) + + def test_env_vars(self): + """Test check_env when setting $WINEPREFIX, $GAMEID and $PROTONPATH.""" + result = None + os.environ["WINEPREFIX"] = self.test_file + os.environ["GAMEID"] = self.test_file + os.environ["PROTONPATH"] = self.test_file + result = ulwgl_run.check_env(self.env) + self.assertTrue(result is self.env, "Expected the same reference") + self.assertEqual( + self.env["WINEPREFIX"], self.test_file, "Expected WINEPREFIX to be set" + ) + self.assertEqual( + self.env["GAMEID"], self.test_file, "Expected GAMEID to be set" + ) + self.assertEqual( + self.env["PROTONPATH"], self.test_file, "Expected PROTONPATH to be set" + ) + + def test_env_vars_proton(self): + """Test check_env when setting only $WINEPREFIX and $GAMEID.""" + with self.assertRaisesRegex(ValueError, "PROTONPATH"): + os.environ["WINEPREFIX"] = self.test_file + os.environ["GAMEID"] = self.test_file + ulwgl_run.check_env(self.env) + + def test_env_vars_wine(self): + """Test check_env when setting only $WINEPREFIX.""" + with self.assertRaisesRegex(ValueError, "GAMEID"): + os.environ["WINEPREFIX"] = self.test_file + ulwgl_run.check_env(self.env) + + def test_env_vars_none(self): + """Tests check_env when setting no env vars.""" + with self.assertRaisesRegex(ValueError, "WINEPREFIX"): + ulwgl_run.check_env(self.env) + + +if __name__ == "__main__": + unittest.main()