left4me/l4d2host/tests/test_install.py
mwiegand d18b397330
fix(host): create ~/.steam/sdk32 and sdk64 symlinks during install
L4D2 dedicated server expects to dlopen steamclient.so via
~/.steam/sdk32 (and sdk64). Without those symlinks, srcds_run logs
'cannot open shared object file' and SteamAPI_Init fails, which means
the server is invisible to the public Steam master server, Workshop
addon downloads break, and Steam 'Join Game' / lobby joins do not
reach the server (only direct-IP connect works).

SteamInstaller.install_or_update now ensures the symlinks exist after
SteamCMD finishes. Targets prefer SteamCMD's linux32/linux64 sibling
dirs; falls back to <install_dir>/bin/ if the siblings cannot be
located. Idempotent: re-running the install repairs or leaves the
symlinks alone.

Path.home() respects HOME, which the systemd web unit sets to
/var/lib/left4me, so the symlinks land in the left4me user's home.

Existing deploys can apply the fix by re-running 'Install' from /admin
without a full redeploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:11:27 +02:00

139 lines
4.7 KiB
Python

import subprocess
import pytest
from l4d2host.steam_install import SteamInstaller
def test_windows_then_linux(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[list[str]] = []
def fake_run_command(cmd, **kwargs):
del kwargs
calls.append(list(cmd))
monkeypatch.setattr("l4d2host.steam_install.run_command", fake_run_command)
SteamInstaller().install_or_update()
assert "windows" in calls[0]
assert "linux" in calls[1]
def test_fail_fast_on_first_failure(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[list[str]] = []
def fake_run_command(cmd, **kwargs):
del kwargs
calls.append(list(cmd))
if len(calls) == 1:
raise subprocess.CalledProcessError(returncode=1, cmd=cmd)
monkeypatch.setattr("l4d2host.steam_install.run_command", fake_run_command)
with pytest.raises(subprocess.CalledProcessError):
SteamInstaller().install_or_update()
assert len(calls) == 1
def test_default_install_dir_uses_left4me_root(tmp_path, monkeypatch):
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
calls = []
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: calls.append(cmd))
SteamInstaller().install_or_update()
assert str(tmp_path / "installation") in calls[0]
def test_steam_installer_emits_steps(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
monkeypatch.setenv("HOME", str(tmp_path / "home"))
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: None)
steps: list[str] = []
from l4d2host.steam_install import SteamInstaller
SteamInstaller().install_or_update(on_stdout=steps.append)
assert steps == [
"Step: downloading windows platform payload...",
"Step: downloading linux platform payload...",
"Step: ensuring steam sdk symlinks...",
"Step: installation complete."
]
def test_install_creates_steam_sdk_symlinks_to_steamcmd_siblings(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
steamcmd_dir = tmp_path / "opt" / "steamcmd"
(steamcmd_dir / "linux32").mkdir(parents=True)
(steamcmd_dir / "linux64").mkdir(parents=True)
fake_steamcmd = steamcmd_dir / "steamcmd.sh"
fake_steamcmd.write_text("#!/bin/sh\nexit 0\n")
fake_steamcmd.chmod(0o755)
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: None)
from l4d2host.steam_install import SteamInstaller
SteamInstaller(steamcmd=str(fake_steamcmd)).install_or_update()
sdk32 = home / ".steam" / "sdk32"
sdk64 = home / ".steam" / "sdk64"
assert sdk32.is_symlink()
assert sdk64.is_symlink()
assert sdk32.resolve() == (steamcmd_dir / "linux32").resolve()
assert sdk64.resolve() == (steamcmd_dir / "linux64").resolve()
def test_install_creates_steam_sdk_symlinks_falls_back_to_install_bin(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
install_bin = tmp_path / "installation" / "bin"
install_bin.mkdir(parents=True)
isolated_dir = tmp_path / "no-siblings"
isolated_dir.mkdir()
fake_steamcmd = isolated_dir / "steamcmd.sh"
fake_steamcmd.write_text("#!/bin/sh\nexit 0\n")
fake_steamcmd.chmod(0o755)
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: None)
from l4d2host.steam_install import SteamInstaller
SteamInstaller(steamcmd=str(fake_steamcmd)).install_or_update()
sdk32 = home / ".steam" / "sdk32"
sdk64 = home / ".steam" / "sdk64"
assert sdk32.resolve() == install_bin.resolve()
assert sdk64.resolve() == install_bin.resolve()
def test_install_steam_sdk_symlinks_is_idempotent(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
steamcmd_dir = tmp_path / "opt" / "steamcmd"
(steamcmd_dir / "linux32").mkdir(parents=True)
(steamcmd_dir / "linux64").mkdir(parents=True)
fake_steamcmd = steamcmd_dir / "steamcmd.sh"
fake_steamcmd.write_text("#!/bin/sh\nexit 0\n")
fake_steamcmd.chmod(0o755)
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: None)
from l4d2host.steam_install import SteamInstaller
installer = SteamInstaller(steamcmd=str(fake_steamcmd))
installer.install_or_update()
installer.install_or_update()
sdk32 = home / ".steam" / "sdk32"
assert sdk32.resolve() == (steamcmd_dir / "linux32").resolve()