"""Unit tests for daemon service file generation and platform detection.""" from __future__ import annotations import subprocess import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest from pyra.daemon.service import ( detect_platform, find_pyra_executable, render_launchd_plist, render_systemd_unit, render_schtasks_xml, ) # ── Template rendering ──────────────────────────────────────────────────────── def test_render_launchd_plist_contains_exe() -> None: xml = render_launchd_plist("/usr/local/bin/pyra", "~/.pyra/daemon.log", "~/.pyra/daemon.pid") assert "/usr/local/bin/pyra" in xml assert "daemon" in xml assert "run" in xml assert "com.pyra.daemon" in xml assert "" in xml # KeepAlive and RunAtLoad def test_render_launchd_plist_expands_log_tilde() -> None: xml = render_launchd_plist("/bin/pyra", "~/.pyra/daemon.log", "~/.pyra/daemon.pid") assert "~" not in xml def test_render_systemd_unit_contains_exe() -> None: unit = render_systemd_unit("/usr/local/bin/pyra", "~/.pyra/daemon.log") assert "ExecStart=/usr/local/bin/pyra daemon run" in unit assert "Restart=on-failure" in unit assert "Type=simple" in unit assert "WantedBy=default.target" in unit def test_render_systemd_unit_expands_log_tilde() -> None: unit = render_systemd_unit("/bin/pyra", "~/.pyra/daemon.log") assert "~" not in unit def test_render_schtasks_xml_contains_exe() -> None: xml = render_schtasks_xml("C:\\Users\\test\\pyra.exe") assert "C:\\Users\\test\\pyra.exe" in xml assert "LogonTrigger" in xml assert "daemon run" in xml assert "IgnoreNew" in xml def test_render_schtasks_xml_no_time_limit() -> None: xml = render_schtasks_xml("pyra.exe") assert "PT0S" in xml # ExecutionTimeLimit=PT0S means unlimited # ── Platform detection ──────────────────────────────────────────────────────── def test_detect_platform_returns_known_value() -> None: result = detect_platform() assert result in ("macos", "linux", "windows") @pytest.mark.parametrize("system,expected", [ ("Darwin", "macos"), ("Linux", "linux"), ("Windows", "windows"), ]) def test_detect_platform_maps_correctly(system: str, expected: str) -> None: with patch("platform.system", return_value=system): assert detect_platform() == expected def test_detect_platform_raises_on_unknown() -> None: with patch("platform.system", return_value="FreeBSD"): with pytest.raises(RuntimeError, match="Unsupported platform"): detect_platform() # ── Executable detection ────────────────────────────────────────────────────── def test_find_pyra_executable_returns_string() -> None: result = find_pyra_executable() assert isinstance(result, str) assert len(result) > 0 def test_find_pyra_executable_uses_which_when_available(tmp_path: Path) -> None: fake_pyra = tmp_path / "pyra" fake_pyra.touch() with patch("shutil.which", return_value=str(fake_pyra)): assert find_pyra_executable() == str(fake_pyra) def test_find_pyra_executable_falls_back_to_sibling(tmp_path: Path) -> None: fake_python = tmp_path / "python3" fake_pyra = tmp_path / "pyra" fake_pyra.touch() with patch("shutil.which", return_value=None): with patch("sys.executable", str(fake_python)): assert find_pyra_executable() == str(fake_pyra) def test_find_pyra_executable_falls_back_to_module(tmp_path: Path) -> None: fake_python = tmp_path / "python3" with patch("shutil.which", return_value=None): with patch("sys.executable", str(fake_python)): result = find_pyra_executable() assert result == f"{fake_python} -m pyra" # ── Install / uninstall (subprocess mocked) ─────────────────────────────────── @pytest.mark.skipif(sys.platform == "win32", reason="launchd install is macOS-only") def test_install_launchd_writes_plist_and_calls_launchctl( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: import pyra.daemon.service as svc plist_path = tmp_path / "Library" / "LaunchAgents" / "com.pyra.daemon.plist" monkeypatch.setattr(svc, "_PLIST_PATH", plist_path) calls: list[list[str]] = [] monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd)) svc._install_launchd("/usr/local/bin/pyra", "~/.pyra/daemon.log", "~/.pyra/daemon.pid") assert plist_path.exists() assert "com.pyra.daemon" in plist_path.read_text() assert any("launchctl" in c[0] for c in calls) @pytest.mark.skipif(sys.platform == "win32", reason="systemd install is Linux-only") def test_install_systemd_writes_unit_and_calls_systemctl( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: import pyra.daemon.service as svc unit_path = tmp_path / ".config" / "systemd" / "user" / "pyra.service" monkeypatch.setattr(svc, "_SYSTEMD_UNIT", unit_path) calls: list[list[str]] = [] monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(cmd)) svc._install_systemd("/usr/local/bin/pyra", "~/.pyra/daemon.log") assert unit_path.exists() assert "ExecStart" in unit_path.read_text() assert any("systemctl" in c[0] for c in calls) @pytest.mark.skipif(sys.platform == "win32", reason="launchd uninstall is macOS-only") def test_uninstall_launchd_removes_plist( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: import pyra.daemon.service as svc plist_path = tmp_path / "Library" / "LaunchAgents" / "com.pyra.daemon.plist" plist_path.parent.mkdir(parents=True) plist_path.write_text("") monkeypatch.setattr(svc, "_PLIST_PATH", plist_path) monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: None) svc._uninstall_launchd() assert not plist_path.exists() @pytest.mark.skipif(sys.platform == "win32", reason="systemd uninstall is Linux-only") def test_uninstall_systemd_removes_unit( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: import pyra.daemon.service as svc unit_path = tmp_path / ".config" / "systemd" / "user" / "pyra.service" unit_path.parent.mkdir(parents=True) unit_path.write_text("[Service]") monkeypatch.setattr(svc, "_SYSTEMD_UNIT", unit_path) monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: None) svc._uninstall_systemd() assert not unit_path.exists()