Files
autossh-mgr/tests/integration/test_lifecycle.py

136 lines
4.1 KiB
Python

import os
import signal
import time
import pytest
from datetime import datetime, timezone
from click.testing import CliRunner
from autossh_mgr.cli import cli
from autossh_mgr.config import save_tunnels, TunnelConfig, ensure_dirs
from autossh_mgr.process import read_pid_file, is_process_alive, write_pid_file
@pytest.fixture
def fake_autossh(tmp_path, monkeypatch):
"""Replace autossh with a long-running dummy process."""
fake = tmp_path / "autossh"
fake.write_text("#!/bin/sh\nexec sleep 60\n")
fake.chmod(0o755)
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
return fake
@pytest.fixture
def config_dir(tmp_path):
ensure_dirs(tmp_path)
return tmp_path
@pytest.fixture(autouse=True)
def set_config_dir(config_dir, monkeypatch):
monkeypatch.setenv("AUTOSSH_MGR_CONFIG_DIR", str(config_dir))
@pytest.fixture(autouse=True)
def cleanup_processes(config_dir):
"""Kill any processes recorded in PID files after each test."""
yield
pids_dir = config_dir / "pids"
if not pids_dir.exists():
return
for pid_file in pids_dir.glob("*.pid"):
try:
lines = pid_file.read_text().splitlines()
if lines:
os.kill(int(lines[0]), signal.SIGKILL)
except (ValueError, OSError, ProcessLookupError):
pass
@pytest.fixture
def tunnel(config_dir):
t = TunnelConfig(
name="test-tunnel",
host="relay.example.com",
user="deploy",
local_port=19999,
remote_port=29999,
)
save_tunnels(config_dir, [t])
return t
@pytest.fixture
def runner():
return CliRunner()
def test_start_writes_pid_file(runner, config_dir, tunnel, fake_autossh):
result = runner.invoke(cli, ["start", "test-tunnel"])
assert result.exit_code == 0, result.output
pid_data = read_pid_file(config_dir, "test-tunnel")
assert pid_data is not None
pid, _ = pid_data
assert is_process_alive(pid)
def test_stop_kills_process(runner, config_dir, tunnel, fake_autossh):
runner.invoke(cli, ["start", "test-tunnel"])
pid_data = read_pid_file(config_dir, "test-tunnel")
assert pid_data is not None
pid = pid_data[0]
assert is_process_alive(pid)
result = runner.invoke(cli, ["stop", "test-tunnel"])
assert result.exit_code == 0
time.sleep(0.2)
assert not is_process_alive(pid)
assert read_pid_file(config_dir, "test-tunnel") is None
def test_status_shows_running(runner, config_dir, tunnel, fake_autossh):
runner.invoke(cli, ["start", "test-tunnel"])
result = runner.invoke(cli, ["status", "test-tunnel"])
assert result.exit_code == 0
assert "running" in result.output
def test_stale_pid_cleaned_on_start(runner, config_dir, tunnel, fake_autossh):
write_pid_file(config_dir, "test-tunnel", 9999999, datetime.now(timezone.utc))
result = runner.invoke(cli, ["start", "test-tunnel"])
assert result.exit_code == 0, result.output
assert "stale" in result.output.lower()
pid_data = read_pid_file(config_dir, "test-tunnel")
assert pid_data is not None
assert pid_data[0] != 9999999
def test_restart(runner, config_dir, tunnel, fake_autossh):
runner.invoke(cli, ["start", "test-tunnel"])
first_pid = read_pid_file(config_dir, "test-tunnel")[0]
result = runner.invoke(cli, ["restart", "test-tunnel"])
assert result.exit_code == 0, result.output
second_pid = read_pid_file(config_dir, "test-tunnel")[0]
assert second_pid != first_pid
assert is_process_alive(second_pid)
assert not is_process_alive(first_pid)
def test_idempotent_start(runner, config_dir, tunnel, fake_autossh):
runner.invoke(cli, ["start", "test-tunnel"])
first_pid = read_pid_file(config_dir, "test-tunnel")[0]
result = runner.invoke(cli, ["start", "test-tunnel"])
assert result.exit_code == 0
assert "already running" in result.output
assert read_pid_file(config_dir, "test-tunnel")[0] == first_pid
def test_idempotent_stop(runner, tunnel):
result = runner.invoke(cli, ["stop", "test-tunnel"])
assert result.exit_code == 0
assert "not running" in result.output