Skip to content

Commit

Permalink
tests: add more unit tests (#274)
Browse files Browse the repository at this point in the history
* umu_run: update get_steam_layer_id parameters

* umu_test: update tests

* umu_test: add test for get_steam_layer_id

* umu_test: add test for create_shim

* umu_test: add test for rearrange_gamescope_baselayer_order

- Tests the case when the app ID from GAMESCOPECTRL_BASELAYER_APPID could not be found in Steam environment variables

* umu_test: add tests for _update_proton

* umu_proton: update docstring

* umu_runtime: don't create shim in check_runtime

- This step is redundant as the shim file is already created before the validation step.

* umu_runtime: don't create shim in _restore_umu

- Redundant step

* umu_runtime: update format

* umu_test: add test for create_shim

* umu_test: update docstring

* umu_test: update test

* umu_test: update assertion
  • Loading branch information
R1kaB3rN authored Nov 14, 2024
1 parent 57d1afe commit aaf2b26
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 21 deletions.
6 changes: 1 addition & 5 deletions umu/umu_proton.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,7 @@ def _update_proton(
protons: list[Path],
thread_pool: ThreadPoolExecutor,
) -> None:
"""Create a symbolic link and remove the previous UMU-Proton.
The symbolic link will be used by clients to reference the PROTONPATH which
can be used for tasks such as killing the running wineserver in the prefix.
The link will be recreated each run.
"""Remove previous stable UMU-Proton builds.
Assumes that the directories that are named ULWGL/UMU-Proton are ours and
will be removed, so users should not be storing important files there.
Expand Down
15 changes: 8 additions & 7 deletions umu/umu_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import zipfile
from _ctypes import CFuncPtr
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from collections.abc import MutableMapping
from concurrent.futures import Future, ThreadPoolExecutor
from contextlib import suppress
from ctypes import CDLL, c_int, c_ulong
Expand Down Expand Up @@ -501,7 +502,7 @@ def rearrange_gamescope_baselayer_order(
"""Rearrange a gamescope base layer sequence retrieved from a window."""
# Note: 'sequence' is actually an array type with unsigned integers
rearranged: list[int] = list(sequence)
steam_layer_id: int = get_steam_layer_id()
steam_layer_id: int = get_steam_layer_id(os.environ)

log.debug("Base layer sequence: %s", sequence)

Expand Down Expand Up @@ -544,24 +545,24 @@ def set_gamescope_baselayer_order(
log.exception(e)


def get_steam_layer_id() -> int:
def get_steam_layer_id(env: MutableMapping) -> int:
"""Get the Steam layer ID from the host environment variables."""
steam_layer_id: int = 0

if path := os.environ.get("STEAM_COMPAT_TRANSCODED_MEDIA_PATH"):
if path := env.get("STEAM_COMPAT_TRANSCODED_MEDIA_PATH"):
# Suppress cases when value is not a number or empty tuple
with suppress(ValueError, IndexError):
return int(Path(path).parts[-1])

if path := os.environ.get("STEAM_COMPAT_MEDIA_PATH"):
if path := env.get("STEAM_COMPAT_MEDIA_PATH"):
with suppress(ValueError, IndexError):
return int(Path(path).parts[-2])

if path := os.environ.get("STEAM_FOSSILIZE_DUMP_PATH"):
if path := env.get("STEAM_FOSSILIZE_DUMP_PATH"):
with suppress(ValueError, IndexError):
return int(Path(path).parts[-3])

if path := os.environ.get("DXVK_STATE_CACHE_PATH"):
if path := env.get("DXVK_STATE_CACHE_PATH"):
with suppress(ValueError, IndexError):
return int(Path(path).parts[-2])

Expand Down Expand Up @@ -623,7 +624,7 @@ def monitor_windows(
) -> None:
"""Monitor for new windows and assign them Steam's layer ID."""
window_ids: set[str] | None = None
steam_assigned_layer_id: int = get_steam_layer_id()
steam_assigned_layer_id: int = get_steam_layer_id(os.environ)

log.debug(
"Waiting for windows under display '%s'...",
Expand Down
8 changes: 2 additions & 6 deletions umu/umu_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def create_shim(file_path: Path | None = None):
# Make the script executable
file_path.chmod(0o700)


def _install_umu(
json: dict[str, Any],
thread_pool: ThreadPoolExecutor,
Expand Down Expand Up @@ -220,6 +221,7 @@ def _install_umu(
# Rename _v2-entry-point
log.debug("Renaming: _v2-entry-point -> umu")
UMU_LOCAL.joinpath("_v2-entry-point").rename(UMU_LOCAL.joinpath("umu"))

create_shim()

# Validate the runtime after moving the files
Expand Down Expand Up @@ -522,9 +524,6 @@ def check_runtime(src: Path, json: dict[str, Any]) -> int:
return ret
log.console(f"{runtime.name}: mtree is OK")

if not UMU_LOCAL.joinpath("umu-shim").exists():
create_shim()

return ret


Expand All @@ -542,6 +541,3 @@ def _restore_umu(
return
_install_umu(json, thread_pool, client_session)
log.debug("Released file lock '%s'", lock.lock_file)

if not UMU_LOCAL.joinpath("umu-shim").exists():
create_shim()
115 changes: 112 additions & 3 deletions umu/umu_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pwd import getpwuid
from shutil import copy, copytree, move, rmtree
from subprocess import CompletedProcess
from tempfile import TemporaryDirectory, mkdtemp
from unittest.mock import MagicMock, patch

sys.path.append(str(Path(__file__).parent.parent))
Expand Down Expand Up @@ -53,6 +54,9 @@ def setUp(self):
"UMU_NO_RUNTIME": "",
"UMU_RUNTIME_UPDATE": "",
"STEAM_COMPAT_TRANSCODED_MEDIA_PATH": "",
"STEAM_COMPAT_MEDIA_PATH": "",
"STEAM_FOSSILIZE_DUMP_PATH": "",
"DXVK_STATE_CACHE_PATH": "",
}
self.user = getpwuid(os.getuid()).pw_name
self.test_opts = "-foo -bar"
Expand Down Expand Up @@ -193,6 +197,83 @@ def tearDown(self):
if self.test_cache_home.exists():
rmtree(self.test_cache_home.as_posix())

def test_get_steam_layer_id(self):
"""Test get_steam_layer_id.
An IndexError and a ValueError should be handled when
Steam environment variables are empty values or non-integers.
"""
os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] = ""
os.environ["STEAM_COMPAT_MEDIA_PATH"] = "foo"
os.environ["STEAM_FOSSILIZE_DUMP_PATH"] = "bar"
os.environ["DXVK_STATE_CACHE_PATH"] = "baz"
result = umu_run.get_steam_layer_id(os.environ)

self.assertEqual(
result,
0,
"Expected 0 when Steam environment variables are empty or non-int",
)

def test_create_shim_exe(self):
"""Test create_shim and ensure it's executable."""
shim = None

with TemporaryDirectory() as tmp:
shim = Path(tmp, "umu-shim")
umu_runtime.create_shim(shim)
self.assertTrue(
os.access(shim, os.X_OK), f"Expected '{shim}' to be executable"
)

def test_create_shim_none(self):
"""Test create_shim when not passed a Path."""
shim = None

# When not passed a Path, the function should default to creating $HOME/.local/share/umu/umu-shim
with (
TemporaryDirectory() as tmp,
patch.object(Path, "joinpath", return_value=Path(tmp, "umu-shim")),
):
umu_runtime.create_shim()
self.assertTrue(
Path(tmp, "umu-shim").is_file(),
f"Expected '{shim}' to be a file",
)
# Ensure there's data
self.assertTrue(
Path(tmp, "umu-shim").stat().st_size > 0,
f"Expected '{shim}' to have data",
)

def test_create_shim(self):
"""Test create_shim."""
shim = None

with TemporaryDirectory() as tmp:
shim = Path(tmp, "umu-shim")
umu_runtime.create_shim(shim)
self.assertTrue(shim.is_file(), f"Expected '{shim}' to be a file")
# Ensure there's data
self.assertTrue(
shim.stat().st_size > 0, f"Expected '{shim}' to have data"
)

def test_rearrange_gamescope_baselayer_order_none(self):
"""Test rearrange_gamescope_baselayer_order for layer ID mismatches."""
steam_window_id = 769
# Mock a real assigned non-Steam app ID
steam_layer_id = 1234
# Mock an overridden value STEAM_COMPAT_TRANSCODED_MEDIA_PATH.
# The app ID for this env var is the last segment and should be found
# in GAMESCOPECTRL_BASELAYER_APPID. When it's not, then that indicates
# it has been tampered by the client or by some middleware.
os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] = "/123"
baselayer = [1, steam_window_id, steam_layer_id]
result = umu_run.rearrange_gamescope_baselayer_order(baselayer)

self.assertTrue(result is None, f"Expected None, received '{result}'")

def test_rearrange_gamescope_baselayer_order_broken(self):
"""Test rearrange_gamescope_baselayer_order when passed broken seq.
Expand All @@ -203,7 +284,7 @@ def test_rearrange_gamescope_baselayer_order_broken(self):
"""
steam_window_id = 769
os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] = "/123"
steam_layer_id = umu_run.get_steam_layer_id()
steam_layer_id = umu_run.get_steam_layer_id(os.environ)
baselayer = [1, steam_window_id, steam_layer_id]
expected = (
[baselayer[0], steam_layer_id, steam_window_id],
Expand All @@ -230,7 +311,7 @@ def test_rearrange_gamescope_baselayer_order(self):
"""Test rearrange_gamescope_baselayer_order when passed a sequence."""
steam_window_id = 769
os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] = "/123"
steam_layer_id = umu_run.get_steam_layer_id()
steam_layer_id = umu_run.get_steam_layer_id(os.environ)
baselayer = [1, steam_layer_id, steam_window_id]
result = umu_run.rearrange_gamescope_baselayer_order(baselayer)

Expand Down Expand Up @@ -473,6 +554,35 @@ def test_move(self):
"qux did not move to dst",
)

def test_update_proton(self):
"""Test _update_proton."""
mock_protons = [Path(mkdtemp()), Path(mkdtemp())]
thread_pool = ThreadPoolExecutor()
result = []

for mock in mock_protons:
self.assertTrue(
mock.is_dir(), f"Directory '{mock}' does not exist"
)

result = umu_proton._update_proton(mock_protons, thread_pool)

self.assertTrue(result is None, f"Expected None, received '{result}'")

# The directories should be removed after the update
for mock in mock_protons:
self.assertFalse(mock.is_dir(), f"Directory '{mock}' still exist")

def test_update_proton_empty(self):
"""Test _update_proton when passed an empty list."""
# In the real usage, an empty list means that there were no
# UMU/ULWGL-Proton found in compatibilitytools.d
result = umu_proton._update_proton([], None)

self.assertTrue(
result is None, "Expected None when passed an empty list"
)

def test_ge_proton(self):
"""Test check_env when the code name GE-Proton is set for PROTONPATH.
Expand Down Expand Up @@ -2379,7 +2489,6 @@ def test_parse_args_winetricks(self):
):
umu_run.parse_args()


def test_parse_args_noopts(self):
"""Test parse_args with no options.
Expand Down

0 comments on commit aaf2b26

Please sign in to comment.