From 1604859f4112c20a53bc6cbc7bb62c7fa9588717 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Wed, 6 May 2026 20:36:24 +0200 Subject: [PATCH] feat(host): add step logging to steam_install --- l4d2host/instances.py | 37 +++++++++++++------------------ l4d2host/logging.py | 8 +++++++ l4d2host/steam_install.py | 3 +++ l4d2host/tests/test_initialize.py | 19 +--------------- l4d2host/tests/test_install.py | 17 ++++++++++++++ l4d2host/tests/test_logging.py | 30 +++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 l4d2host/logging.py create mode 100644 l4d2host/tests/test_logging.py diff --git a/l4d2host/instances.py b/l4d2host/instances.py index d6218a6..59aa8c1 100644 --- a/l4d2host/instances.py +++ b/l4d2host/instances.py @@ -8,12 +8,7 @@ from l4d2host.service_control import start_service, stop_service from l4d2host.spec import load_spec -def _emit_step(msg: str, on_stdout: Callable[[str], None] | None, passthrough: bool) -> None: - formatted = f"Step: {msg}" - if on_stdout is not None: - on_stdout(formatted) - elif passthrough: - print(formatted, flush=True) +from l4d2host.logging import emit_step DEFAULT_ROOT = DEFAULT_LEFT4ME_ROOT @@ -32,7 +27,7 @@ def initialize_instance( root = get_left4me_root() if root is None else Path(root) spec = load_spec(spec_path) - _emit_step("creating instance directories...", on_stdout, passthrough) + emit_step("creating instance directories...", on_stdout, passthrough) instance_dir = root / "instances" / name runtime_dir = root / "runtime" / name (runtime_dir / "upper").mkdir(parents=True, exist_ok=True) @@ -43,7 +38,7 @@ def initialize_instance( lowerdirs = [str(overlay_path(overlay, root=root)) for overlay in spec.overlays] lowerdirs.append(str(root / "installation")) - _emit_step("writing instance.env...", on_stdout, passthrough) + emit_step("writing instance.env...", on_stdout, passthrough) instance_env = "\n".join( [ f"L4D2_PORT={spec.port}", @@ -53,10 +48,10 @@ def initialize_instance( ) + "\n" (instance_dir / "instance.env").write_text(instance_env) - _emit_step("writing server.cfg...", on_stdout, passthrough) + emit_step("writing server.cfg...", on_stdout, passthrough) server_cfg = "\n".join(spec.config) if spec.config else "" (instance_dir / "server.cfg").write_text(server_cfg) - _emit_step("initialization complete.", on_stdout, passthrough) + emit_step("initialization complete.", on_stdout, passthrough) def _load_instance_env(path: Path) -> dict[str, str]: @@ -84,7 +79,7 @@ def start_instance( env = _load_instance_env(instance_dir / "instance.env") - _emit_step("mounting runtime overlay...", on_stdout, passthrough) + emit_step("mounting runtime overlay...", on_stdout, passthrough) run_command( [ "fuse-overlayfs", @@ -102,12 +97,12 @@ def start_instance( should_cancel=should_cancel, ) - _emit_step("copying server.cfg to runtime...", on_stdout, passthrough) + emit_step("copying server.cfg to runtime...", on_stdout, passthrough) target_cfg = runtime_dir / "merged" / "left4dead2" / "cfg" / "server.cfg" target_cfg.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(instance_dir / "server.cfg", target_cfg) - _emit_step("starting systemd service...", on_stdout, passthrough) + emit_step("starting systemd service...", on_stdout, passthrough) start_service( name, on_stdout=on_stdout, @@ -115,7 +110,7 @@ def start_instance( passthrough=passthrough, should_cancel=should_cancel, ) - _emit_step("start complete.", on_stdout, passthrough) + emit_step("start complete.", on_stdout, passthrough) def stop_instance( @@ -128,7 +123,7 @@ def stop_instance( should_cancel: Callable[[], bool] | None = None, ) -> None: root = get_left4me_root() if root is None else Path(root) - _emit_step("stopping systemd service...", on_stdout, passthrough) + emit_step("stopping systemd service...", on_stdout, passthrough) stop_service( name, on_stdout=on_stdout, @@ -136,7 +131,7 @@ def stop_instance( passthrough=passthrough, should_cancel=should_cancel, ) - _emit_step("unmounting runtime overlay...", on_stdout, passthrough) + emit_step("unmounting runtime overlay...", on_stdout, passthrough) run_command( ["fusermount3", "-u", str(root / "runtime" / name / "merged")], on_stdout=on_stdout, @@ -144,7 +139,7 @@ def stop_instance( passthrough=passthrough, should_cancel=should_cancel, ) - _emit_step("stop complete.", on_stdout, passthrough) + emit_step("stop complete.", on_stdout, passthrough) def delete_instance( @@ -163,7 +158,7 @@ def delete_instance( if not instance_dir.exists() and not runtime_dir.exists(): return - _emit_step("stopping systemd service (if running)...", on_stdout, passthrough) + emit_step("stopping systemd service (if running)...", on_stdout, passthrough) stop_service( name, on_stdout=on_stdout, @@ -174,7 +169,7 @@ def delete_instance( merged = runtime_dir / "merged" if merged.is_mount(): - _emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough) + emit_step("unmounting runtime overlay (if mounted)...", on_stdout, passthrough) run_command( ["fusermount3", "-u", str(merged)], on_stdout=on_stdout, @@ -183,9 +178,9 @@ def delete_instance( should_cancel=should_cancel, ) - _emit_step("removing instance files...", on_stdout, passthrough) + emit_step("removing instance files...", on_stdout, passthrough) if instance_dir.exists(): shutil.rmtree(instance_dir) if runtime_dir.exists(): shutil.rmtree(runtime_dir) - _emit_step("delete complete.", on_stdout, passthrough) + emit_step("delete complete.", on_stdout, passthrough) diff --git a/l4d2host/logging.py b/l4d2host/logging.py new file mode 100644 index 0000000..0cdb5fd --- /dev/null +++ b/l4d2host/logging.py @@ -0,0 +1,8 @@ +from typing import Callable + +def emit_step(msg: str, on_stdout: Callable[[str], None] | None, passthrough: bool) -> None: + formatted = f"Step: {msg}" + if on_stdout is not None: + on_stdout(formatted) + if passthrough: + print(formatted, flush=True) diff --git a/l4d2host/steam_install.py b/l4d2host/steam_install.py index 0f6cf2b..98c3df3 100644 --- a/l4d2host/steam_install.py +++ b/l4d2host/steam_install.py @@ -3,6 +3,7 @@ from typing import Callable from l4d2host.paths import get_left4me_root from l4d2host.process import run_command +from l4d2host.logging import emit_step class SteamInstaller: @@ -19,6 +20,7 @@ class SteamInstaller: should_cancel: Callable[[], bool] | None = None, ) -> None: for platform in ("windows", "linux"): + emit_step(f"downloading {platform} platform payload...", on_stdout, passthrough) run_command( [ self.steamcmd, @@ -38,3 +40,4 @@ class SteamInstaller: passthrough=passthrough, should_cancel=should_cancel, ) + emit_step("installation complete.", on_stdout, passthrough) diff --git a/l4d2host/tests/test_initialize.py b/l4d2host/tests/test_initialize.py index 303859f..53d23a3 100644 --- a/l4d2host/tests/test_initialize.py +++ b/l4d2host/tests/test_initialize.py @@ -2,7 +2,7 @@ import sys from io import StringIO from pathlib import Path -from l4d2host.instances import initialize_instance, _emit_step +from l4d2host.instances import initialize_instance def test_initialize_writes_files(tmp_path: Path) -> None: @@ -35,23 +35,6 @@ def test_initialize_uses_configured_left4me_root(tmp_path: Path, monkeypatch) -> assert f"L4D2_LOWERDIRS={tmp_path}/overlays/standard:{tmp_path}/installation" in env -def test_emit_step_uses_callback() -> None: - calls: list[str] = [] - _emit_step("test step", on_stdout=calls.append, passthrough=False) - assert calls == ["Step: test step"] - - -def test_emit_step_uses_passthrough_stdout(monkeypatch) -> None: - fake_out = StringIO() - monkeypatch.setattr(sys, "stdout", fake_out) - _emit_step("passthrough step", on_stdout=None, passthrough=True) - assert fake_out.getvalue() == "Step: passthrough step\n" - - -def test_emit_step_does_nothing_if_no_target() -> None: - _emit_step("silent step", on_stdout=None, passthrough=False) - - def test_initialize_instance_emits_steps(tmp_path: Path) -> None: spec = tmp_path / "spec.yaml" spec.write_text("port: 27015\noverlays: [standard]\n") diff --git a/l4d2host/tests/test_install.py b/l4d2host/tests/test_install.py index 89ea2e7..5f7fa30 100644 --- a/l4d2host/tests/test_install.py +++ b/l4d2host/tests/test_install.py @@ -43,3 +43,20 @@ def test_default_install_dir_uses_left4me_root(tmp_path, monkeypatch): SteamInstaller().install_or_update() assert str(tmp_path / "installation") in calls[0] + + +def test_steam_installer_emits_steps(tmp_path, monkeypatch) -> None: + from pathlib import Path + monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path)) + 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: installation complete." + ] diff --git a/l4d2host/tests/test_logging.py b/l4d2host/tests/test_logging.py new file mode 100644 index 0000000..5996a79 --- /dev/null +++ b/l4d2host/tests/test_logging.py @@ -0,0 +1,30 @@ +import sys +from io import StringIO + +from l4d2host.logging import emit_step + + +def test_emit_step_uses_callback() -> None: + calls: list[str] = [] + emit_step("test step", on_stdout=calls.append, passthrough=False) + assert calls == ["Step: test step"] + + +def test_emit_step_uses_passthrough_stdout(monkeypatch) -> None: + fake_out = StringIO() + monkeypatch.setattr(sys, "stdout", fake_out) + emit_step("passthrough step", on_stdout=None, passthrough=True) + assert fake_out.getvalue() == "Step: passthrough step\n" + + +def test_emit_step_does_nothing_if_no_target() -> None: + emit_step("silent step", on_stdout=None, passthrough=False) + + +def test_emit_step_does_both(monkeypatch) -> None: + calls: list[str] = [] + fake_out = StringIO() + monkeypatch.setattr(sys, "stdout", fake_out) + emit_step("both step", on_stdout=calls.append, passthrough=True) + assert calls == ["Step: both step"] + assert fake_out.getvalue() == "Step: both step\n"