diff --git a/ulwgl_dl_util.py b/ulwgl_dl_util.py new file mode 100644 index 000000000..aa13761f6 --- /dev/null +++ b/ulwgl_dl_util.py @@ -0,0 +1,258 @@ +from pathlib import Path +from os import environ +from tarfile import open as tar_open +from typing import Dict, List, Tuple, Any, Union +from hashlib import sha512 +from shutil import rmtree +from http.client import HTTPSConnection, HTTPResponse, HTTPException, HTTPConnection +from ssl import create_default_context +from json import loads as loads_json +from urllib.request import urlretrieve + + +def get_ulwgl_proton(env: Dict[str, str]) -> Union[Dict[str, str]]: + """Attempt to find existing Proton from the system or downloads the latest if PROTONPATH is not set. + + Only fetches the latest if not first found in .local/share/Steam/compatibilitytools.d + .cache/ULWGL is referenced for the latest then as fallback + """ + files: List[Tuple[str, str]] = [] + + try: + files = _fetch_releases() + except HTTPException: + print("Offline.\nContinuing ...") + + cache: Path = Path.home().joinpath(".cache/ULWGL") + steam_compat: Path = Path.home().joinpath(".local/share/Steam/compatibilitytools.d") + + cache.mkdir(exist_ok=True, parents=True) + steam_compat.mkdir(exist_ok=True, parents=True) + + # Prioritize the Steam compat + if _get_from_steamcompat(env, steam_compat, cache, files): + return env + + # Use the latest Proton in the cache if it exists + if _get_from_cache(env, steam_compat, cache, files, True): + return env + + # Download the latest if Proton is not in Steam compat + # If the digests mismatched, refer to the cache in the next block + if _get_latest(env, steam_compat, cache, files): + return env + + # Refer to an old version previously downloaded + # Reached on digest mismatch, user interrupt or download failure/no internet + if _get_from_cache(env, steam_compat, cache, files, False): + return env + + # No internet and cache/compat tool is empty, just return and raise an exception from the caller + return env + + +def _fetch_releases() -> List[Tuple[str, str]]: + """Fetch the latest releases from the Github API.""" + files: List[Tuple[str, str]] = [] + resp: HTTPResponse = None + conn: HTTPConnection = HTTPSConnection( + "api.github.com", timeout=30, context=create_default_context() + ) + + conn.request( + "GET", + "/repos/Open-Wine-Components/ULWGL-Proton/releases", + headers={ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "", + }, + ) + + resp = conn.getresponse() + + if resp and resp.status != 200: + return files + + # Attempt to acquire the tarball and checksum from the JSON data + releases: List[Dict[str, Any]] = loads_json(resp.read().decode("utf-8")) + for release in releases: + if "assets" in release: + assets: List[Dict[str, Any]] = release["assets"] + + for asset in assets: + if ( + "name" in asset + and ( + asset["name"].endswith("sum") + or ( + asset["name"].endswith("tar.gz") + and asset["name"].startswith("ULWGL-Proton") + ) + ) + and "browser_download_url" in asset + ): + if asset["name"].endswith("sum"): + files.append((asset["name"], asset["browser_download_url"])) + else: + files.append((asset["name"], asset["browser_download_url"])) + + if len(files) == 2: + break + break + + return files + + +def _fetch_proton( + env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]] +) -> Dict[str, str]: + """Download the latest ULWGL-Proton and set it as PROTONPATH.""" + hash, hash_url = files[0] + proton, proton_url = files[1] + proton_dir: str = proton[: proton.find(".tar.gz")] # Proton dir + + # TODO: Parallelize this + print(f"Downloading {hash} ...") + urlretrieve(hash_url, cache.joinpath(hash).as_posix()) + print(f"Downloading {proton} ...") + urlretrieve(proton_url, cache.joinpath(proton).as_posix()) + + print("Completed.") + + with cache.joinpath(proton).open(mode="rb") as file: + if ( + sha512(file.read()).hexdigest() + != cache.joinpath(hash).read_text().split(" ")[0] + ): + err: str = "Digests mismatched.\nFalling back to cache ..." + raise ValueError(err) + print(f"{proton}: SHA512 is OK") + + _extract_dir(cache.joinpath(proton), steam_compat) + environ["PROTONPATH"] = steam_compat.joinpath(proton_dir).as_posix() + env["PROTONPATH"] = environ["PROTONPATH"] + + return env + + +def _extract_dir(proton: Path, steam_compat: Path) -> None: + """Extract from the cache to another location.""" + with tar_open(proton.as_posix(), "r:gz") as tar: + print(f"Extracting {proton} -> {steam_compat.as_posix()} ...") + tar.extractall(path=steam_compat.as_posix()) + print("Completed.") + + +def _cleanup(tarball: str, proton: str, cache: Path, steam_compat: Path) -> None: + """Remove files that may have been left in an incomplete state to avoid corruption. + + We want to do this when a download for a new release is interrupted + """ + print("Keyboard Interrupt.\nCleaning ...") + + if cache.joinpath(tarball).is_file(): + print(f"Purging {tarball} in {cache} ...") + cache.joinpath(tarball).unlink() + if steam_compat.joinpath(proton).is_dir(): + print(f"Purging {proton} in {steam_compat} ...") + rmtree(steam_compat.joinpath(proton).as_posix()) + + +def _get_from_steamcompat( + env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]] +) -> Union[Dict[str, str], None]: + """Refer to Steam compat folder for any existing Proton directories.""" + proton_dir: str = "" # Latest Proton + + if len(files) == 2: + proton_dir: str = files[1][0][: files[1][0].find(".tar.gz")] + + for proton in steam_compat.glob("ULWGL-Proton*"): + print(f"{proton.name} found in: {steam_compat.as_posix()}") + environ["PROTONPATH"] = proton.as_posix() + env["PROTONPATH"] = environ["PROTONPATH"] + + # Notify the user that they're not using the latest + if proton_dir and proton.name != proton_dir: + print( + "ULWGL-Proton is outdated.\nFor latest release, please download " + + files[1][1] + ) + + return env + + return None + + +def _get_from_cache( + env: Dict[str, str], + steam_compat: Path, + cache: Path, + files: List[Tuple[str, str]], + use_latest=True, +) -> Union[Dict[str, str], None]: + """Refer to ULWGL cache directory. + + Use the latest in the cache when present. When download fails, use an old version + Older Proton versions are only referred to when: digests mismatch, user interrupt, or download failure/no internet + """ + path: Path = None + name: str = "" + + for tarball in cache.glob("ULWGL-Proton*.tar.gz"): + if files and tarball == cache.joinpath(files[1][0]) and use_latest: + path = tarball + name = tarball.name + break + if tarball != cache.joinpath(files[1][0]) and not use_latest: + path = tarball + name = tarball.name + break + + if path: + proton_dir: str = name[: name.find(".tar.gz")] # Proton dir + + print(f"{name} found in: {path}") + try: + _extract_dir(path, steam_compat) + environ["PROTONPATH"] = steam_compat.joinpath(proton_dir).as_posix() + env["PROTONPATH"] = environ["PROTONPATH"] + + return env + except KeyboardInterrupt: + if steam_compat.joinpath(proton_dir).is_dir(): + print(f"Purging {proton_dir} in {steam_compat} ...") + rmtree(steam_compat.joinpath(proton_dir).as_posix()) + raise + + return None + + +def _get_latest( + env: Dict[str, str], steam_compat: Path, cache: Path, files: List[Tuple[str, str]] +) -> Union[Dict[str, str], None]: + """Download the latest Proton for new installs -- empty cache and Steam compat. + + When the digests mismatched or when interrupted, refer to cache for an old version + """ + if files: + print("Fetching latest release ...") + try: + _fetch_proton(env, steam_compat, cache, files) + env["PROTONPATH"] = environ["PROTONPATH"] + except ValueError: + # Digest mismatched or download failed + # Refer to the cache for old version next + return None + except KeyboardInterrupt: + tarball: str = files[1][0] + proton_dir: str = tarball[: tarball.find(".tar.gz")] # Proton dir + + # Exit cleanly + # Clean up extracted data and cache to prevent corruption/errors + # Refer to the cache for old version next + _cleanup(tarball, proton_dir, cache, steam_compat) + return None + + return env diff --git a/ulwgl_run.py b/ulwgl_run.py index d8484f61f..41a2532b9 100755 --- a/ulwgl_run.py +++ b/ulwgl_run.py @@ -10,6 +10,7 @@ import ulwgl_plugins from re import match import subprocess +from ulwgl_dl_util import get_ulwgl_proton def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103 @@ -118,9 +119,16 @@ def check_env( "PROTONPATH" not in os.environ or not Path(os.environ["PROTONPATH"]).expanduser().is_dir() ): - err: str = "Environment variable not set or not a directory: PROTONPATH" - raise ValueError(err) - env["PROTONPATH"] = os.environ["PROTONPATH"] + # Attempt to auto set this env var for the user + os.environ["PROTONPATH"] = "" + get_ulwgl_proton(env) + else: + env["PROTONPATH"] = os.environ["PROTONPATH"] + + # If download fails/doesn't exist in the system, raise an error + if not os.environ["PROTONPATH"]: + err: str = "Download failed.\nProton could not be found in cache or compatibilitytools.d\nPlease set $PROTONPATH or visit https://github.com/Open-Wine-Components/ULWGL-Proton/releases" + raise FileNotFoundError(err) return env diff --git a/ulwgl_test.py b/ulwgl_test.py index 75fefdc8d..e03190290 100644 --- a/ulwgl_test.py +++ b/ulwgl_test.py @@ -9,6 +9,8 @@ from shutil import rmtree import re import ulwgl_plugins +import ulwgl_dl_util +import tarfile class TestGameLauncher(unittest.TestCase): @@ -49,6 +51,34 @@ def setUp(self): self.test_file = "./tmp.WMYQiPb9A" # Executable self.test_exe = self.test_file + "/" + "foo" + # Cache + self.test_cache = Path("./tmp.5HYdpddgvs") + # Steam compat dir + self.test_compat = Path("./tmp.ZssGZoiNod") + # ULWGL-Proton dir + self.test_proton_dir = Path("ULWGL-Proton-5HYdpddgvs") + # ULWGL-Proton release + self.test_archive = Path(self.test_cache).joinpath( + f"{self.test_proton_dir}.tar.gz" + ) + + self.test_cache.mkdir(exist_ok=True) + self.test_compat.mkdir(exist_ok=True) + self.test_proton_dir.mkdir(exist_ok=True) + + # Mock the proton file in the dir + self.test_proton_dir.joinpath("proton").touch(exist_ok=True) + + # Mock the release downloaded in the cache: tmp.5HYdpddgvs/ULWGL-Proton-5HYdpddgvs.tar.gz + # Expected directory structure within the archive: + # + # +-- ULWGL-Proton-5HYdpddgvs (root directory) + # | +-- proton (normal file) + with tarfile.open(self.test_archive.as_posix(), "w:gz") as tar: + tar.add( + self.test_proton_dir.as_posix(), arcname=self.test_proton_dir.as_posix() + ) + Path(self.test_file).mkdir(exist_ok=True) Path(self.test_exe).touch() @@ -61,6 +91,365 @@ def tearDown(self): if Path(self.test_file).exists(): rmtree(self.test_file) + if self.test_cache.exists(): + rmtree(self.test_cache.as_posix()) + + if self.test_compat.exists(): + rmtree(self.test_compat.as_posix()) + + if self.test_proton_dir.exists(): + rmtree(self.test_proton_dir.as_posix()) + + def test_latest_interrupt(self): + """Test _get_latest in the event the user interrupts the download/extraction process. + + Assumes a file is being downloaded or extracted in this case. + A KeyboardInterrupt should be raised, and the cache/compat dir should be cleaned afterwards. + """ + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # In this case, assume the test variable will be downloaded + files = [("", ""), (self.test_archive.name, "")] + + # In the event of an interrupt, both the cache/compat dir will be checked for the latest release for removal + # We do this since the extraction process can be interrupted as well + ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + + with patch("ulwgl_dl_util._fetch_proton") as mock_function: + # Mock the interrupt + # We want the dir we tried to extract to be cleaned + mock_function.side_effect = KeyboardInterrupt + result = ulwgl_dl_util._get_latest( + self.env, self.test_compat, self.test_cache, files + ) + self.assertFalse(self.env["PROTONPATH"], "Expected PROTONPATH to be empty") + self.assertFalse(result, "Expected None when a ValueError occurs") + + # Verify the state of the compat dir/cache + self.assertFalse( + self.test_compat.joinpath( + self.test_archive.name[: self.test_archive.name.find(".tar.gz")] + ).exists(), + "Expected Proton dir in compat to be cleaned", + ) + self.assertFalse( + self.test_cache.joinpath(self.test_archive.name).exists(), + "Expected Proton dir in compat to be cleaned", + ) + + def test_latest_val_err(self): + """Test _get_latest in the event something goes wrong in the download process for the latest Proton. + + Assumes a file is being downloaded in this case. + A ValueError should be raised, and one case it can happen is if the digests mismatched for some reason + """ + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # When empty, it means the callout failed for some reason (e.g. no internet) + files = [("", ""), (self.test_archive.name, "")] + + with patch("ulwgl_dl_util._fetch_proton") as mock_function: + # Mock the interrupt + mock_function.side_effect = ValueError + result = ulwgl_dl_util._get_latest( + self.env, self.test_compat, self.test_cache, files + ) + self.assertFalse(self.env["PROTONPATH"], "Expected PROTONPATH to be empty") + self.assertFalse(result, "Expected None when a ValueError occurs") + + def test_latest_offline(self): + """Test _get_latest when the user doesn't have internet.""" + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # When empty, it means the callout failed for some reason (e.g. no internet) + files = [] + + os.environ["PROTONPATH"] = "" + + with patch("ulwgl_dl_util._fetch_proton"): + result = ulwgl_dl_util._get_latest( + self.env, self.test_compat, self.test_cache, files + ) + self.assertFalse(self.env["PROTONPATH"], "Expected PROTONPATH to be empty") + self.assertTrue(result is self.env, "Expected the same reference") + + def test_cache_interrupt(self): + """Test _get_from_cache on keyboard interrupt on extraction from the cache to the compat dir.""" + # In the real usage, should be populated after successful callout for latest Proton releases + # Just mock it and assumes its the latest + files = [("", ""), (self.test_archive.name, "")] + + ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + + self.assertTrue( + self.test_compat.joinpath( + self.test_archive.name[: self.test_archive.name.find(".tar.gz")] + ).exists(), + "Expected Proton dir to exist in compat", + ) + + with patch("ulwgl_dl_util._extract_dir") as mock_function: + with self.assertRaisesRegex(KeyboardInterrupt, ""): + # Mock the interrupt + # We want to simulate an interrupt mid-extraction in this case + # We want the dir we tried to extract to be cleaned + mock_function.side_effect = KeyboardInterrupt + ulwgl_dl_util._get_from_cache( + self.env, self.test_compat, self.test_cache, files, True + ) + + # After interrupt, we attempt to clean the compat dir for the file we tried to extract because it could be in an incomplete state + # Verify that the dir we tried to extract from cache is removed to avoid corruption on next launch + self.assertFalse( + self.test_compat.joinpath( + self.test_archive.name[: self.test_archive.name.find(".tar.gz")] + ).exists(), + "Expected Proton dir in compat to be cleaned", + ) + + def test_cache_old(self): + """Test _get_from_cache when the cache is empty. + + In real usage, this only happens as a last resort when: download fails, digests mismatched, etc. + """ + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # Just mock it and assumes its the latest + files = [("", ""), (self.test_archive.name, "")] + + # Mock old Proton versions in the cache + test_proton_dir = Path("ULWGL-Proton-foo") + test_proton_dir.mkdir(exist_ok=True) + test_archive = Path(self.test_cache).joinpath( + f"{test_proton_dir.as_posix()}.tar.gz" + ) + + with tarfile.open(test_archive.as_posix(), "w:gz") as tar: + tar.add(test_proton_dir.as_posix(), arcname=test_proton_dir.as_posix()) + + result = ulwgl_dl_util._get_from_cache( + self.env, self.test_compat, self.test_cache, files, False + ) + + # Verify that the old Proton was assigned + self.assertTrue(result is self.env, "Expected the same reference") + self.assertEqual( + self.env["PROTONPATH"], + self.test_compat.joinpath( + test_archive.name[: test_archive.name.find(".tar.gz")] + ).as_posix(), + "Expected PROTONPATH to be proton dir in compat", + ) + + test_archive.unlink() + test_proton_dir.rmdir() + + def test_cache_empty(self): + """Test _get_from_cache when the cache is empty.""" + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # Just mock it and assumes its the latest + files = [("", ""), (self.test_archive.name, "")] + + self.test_archive.unlink() + + result = ulwgl_dl_util._get_from_cache( + self.env, self.test_compat, self.test_cache, files, True + ) + self.assertFalse(result, "Expected None when calling _get_from_cache") + self.assertFalse( + self.env["PROTONPATH"], + "Expected PROTONPATH to be empty when the cache is empty", + ) + + def test_cache(self): + """Test _get_from_cache. + + Tests the case when the latest Proton already exists in the cache + """ + result = None + # In the real usage, should be populated after successful callout for latest Proton releases + # Just mock it and assumes its the latest + files = [("", ""), (self.test_archive.name, "")] + + result = ulwgl_dl_util._get_from_cache( + self.env, self.test_compat, self.test_cache, files, True + ) + self.assertTrue(result is self.env, "Expected the same reference") + self.assertEqual( + self.env["PROTONPATH"], + self.test_compat.joinpath( + self.test_archive.name[: self.test_archive.name.find(".tar.gz")] + ).as_posix(), + "Expected PROTONPATH to be proton dir in compat", + ) + + def test_steamcompat_nodir(self): + """Test _get_from_steamcompat when a Proton doesn't exist in the Steam compat dir. + + In this case, the None should be returned to signal that we should continue with downloading the latest Proton + """ + result = None + files = [("", ""), (self.test_archive.name, "")] + + result = ulwgl_dl_util._get_from_steamcompat( + self.env, self.test_compat, self.test_cache, files + ) + + self.assertFalse(result, "Expected None after calling _get_from_steamcompat") + self.assertFalse(self.env["PROTONPATH"], "Expected PROTONPATH to not be set") + + def test_steamcompat(self): + """Test _get_from_steamcompat. + + When a Proton exist in .local/share/Steam/compatibilitytools.d, use it when PROTONPATH is unset + """ + result = None + files = [("", ""), (self.test_archive.name, "")] + + ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + + result = ulwgl_dl_util._get_from_steamcompat( + self.env, self.test_compat, self.test_cache, files + ) + + self.assertTrue(result is self.env, "Expected the same reference") + self.assertEqual( + self.env["PROTONPATH"], + self.test_compat.joinpath( + self.test_archive.name[: self.test_archive.name.find(".tar.gz")] + ).as_posix(), + "Expected PROTONPATH to be proton dir in compat", + ) + + def test_cleanup_no_exists(self): + """Test _cleanup when passed files that do not exist. + + In the event of an interrupt during the download/extract process, we only want to clean the files that exist + NOTE: This is **extremely** important, as we do **not** want to delete anything else but the files we downloaded/extracted -- the incomplete tarball/extracted dir + """ + result = None + + ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + + # Create a file in the cache and compat + self.test_cache.joinpath("foo").touch() + self.test_compat.joinpath("foo").touch() + + # Before cleaning + # On setUp, an archive is created and a dir should exist in compat after extraction + self.assertTrue( + self.test_compat.joinpath("foo").exists(), + "Expected test file to exist in compat before cleaning", + ) + self.assertTrue( + self.test_cache.joinpath("foo").exists(), + "Expected test file to exist in cache before cleaning", + ) + self.assertTrue( + self.test_archive.exists(), + "Expected archive to exist in cache before cleaning", + ) + self.assertTrue( + self.test_compat.joinpath(self.test_proton_dir).joinpath("proton").exists(), + "Expected 'proton' to exist before cleaning", + ) + + # Pass files that do not exist + result = ulwgl_dl_util._cleanup( + "foo.tar.gz", + "foo", + self.test_cache, + self.test_compat, + ) + + # Verify state of cache and compat after cleaning + self.assertFalse(result, "Expected None after cleaning") + self.assertTrue( + self.test_compat.joinpath("foo").exists(), + "Expected test file to exist in compat after cleaning", + ) + self.assertTrue( + self.test_cache.joinpath("foo").exists(), + "Expected test file to exist in cache after cleaning", + ) + self.assertTrue( + self.test_compat.joinpath(self.test_proton_dir).exists(), + "Expected proton dir to still exist after cleaning", + ) + self.assertTrue( + self.test_archive.exists(), + "Expected archive to still exist after cleaning", + ) + self.assertTrue( + self.test_compat.joinpath(self.test_proton_dir).joinpath("proton").exists(), + "Expected 'proton' to still exist after cleaning", + ) + + def test_cleanup(self): + """Test _cleanup. + + In the event of an interrupt during the download/extract process, we want to clean the cache or the extracted dir in Steam compat to avoid incomplete files + """ + result = None + + ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + result = ulwgl_dl_util._cleanup( + self.test_proton_dir.as_posix() + ".tar.gz", + self.test_proton_dir.as_posix(), + self.test_cache, + self.test_compat, + ) + self.assertFalse(result, "Expected None after cleaning") + self.assertFalse( + self.test_compat.joinpath(self.test_proton_dir).exists(), + "Expected proton dir to be cleaned in compat", + ) + self.assertFalse( + self.test_archive.exists(), + "Expected archive to be cleaned in cache", + ) + self.assertFalse( + self.test_compat.joinpath(self.test_proton_dir).joinpath("proton").exists(), + "Expected 'proton' to not exist after cleaned", + ) + + def test_extract_err(self): + """Test _extract_dir when passed a non-gzip compressed archive. + + An error should be raised as we only expect .tar.gz releases + """ + test_archive = self.test_cache.joinpath(f"{self.test_proton_dir}.tar") + # Do not apply compression + with tarfile.open(test_archive.as_posix(), "w") as tar: + tar.add( + self.test_proton_dir.as_posix(), arcname=self.test_proton_dir.as_posix() + ) + + with self.assertRaisesRegex(tarfile.ReadError, "gzip"): + ulwgl_dl_util._extract_dir(test_archive, self.test_compat) + + if test_archive.exists(): + test_archive.unlink() + + def test_extract(self): + """Test _extract_dir. + + An error should not be raised when the Proton release is extracted to the Steam compat dir + """ + result = None + + result = ulwgl_dl_util._extract_dir(self.test_archive, self.test_compat) + self.assertFalse(result, "Expected None after extracting") + self.assertTrue( + self.test_compat.joinpath(self.test_proton_dir).exists(), + "Expected proton dir to exists in compat", + ) + self.assertTrue( + self.test_compat.joinpath(self.test_proton_dir).joinpath("proton").exists(), + "Expected 'proton' file to exists in the proton dir", + ) + def test_game_drive_empty(self): """Test enable_steam_game_drive. @@ -1037,20 +1426,27 @@ def test_parse_args_config(self): result, Namespace, "Expected a Namespace from parse_arg" ) - def test_env_proton_dir(self): - """Test check_env when $PROTONPATH is not a directory. + def test_env_proton_nodir(self): + """Test check_env when $PROTONPATH is not set on failing to setting it. - An ValueError should occur if the value is not a directory + An FileNotFoundError should be raised when we fail to set PROTONPATH """ - 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", - ) + result = None + + # Mock getting the Proton + with self.assertRaises(FileNotFoundError): + with patch.object( + ulwgl_run, + "get_ulwgl_proton", + return_value=self.env, + ): + os.environ["WINEPREFIX"] = self.test_file + os.environ["GAMEID"] = self.test_file + result = ulwgl_run.check_env(self.env) + # Mock setting it on success + os.environ["PROTONPATH"] = self.test_file + self.assertTrue(result is self.env, "Expected the same reference") + self.assertFalse(os.environ["PROTONPATH"]) def test_env_wine_dir(self): """Test check_env when $WINEPREFIX is not a directory. @@ -1119,10 +1515,20 @@ def test_env_vars(self): def test_env_vars_proton(self): """Test check_env when setting only $WINEPREFIX and $GAMEID.""" - with self.assertRaisesRegex(ValueError, "PROTONPATH"): + with self.assertRaisesRegex(FileNotFoundError, "Proton"): os.environ["WINEPREFIX"] = self.test_file os.environ["GAMEID"] = self.test_file - ulwgl_run.check_env(self.env) + # Mock getting the Proton + with patch.object( + ulwgl_run, + "get_ulwgl_proton", + return_value=self.env, + ): + os.environ["WINEPREFIX"] = self.test_file + os.environ["GAMEID"] = self.test_file + result = ulwgl_run.check_env(self.env) + self.assertTrue(result is self.env, "Expected the same reference") + self.assertFalse(os.environ["PROTONPATH"]) def test_env_vars_wine(self): """Test check_env when setting only $WINEPREFIX."""