mcqemu/tests/conftest.py

110 lines
3.1 KiB
Python

"""Shared fixtures: temp XDG dirs, scriptable fake QMP client, in-memory MCP client."""
import asyncio
import json
from types import SimpleNamespace
import pytest
from fastmcp import Client
from mcqemu.server import mcp
@pytest.fixture
def dirs(tmp_path, monkeypatch):
state = tmp_path / "state"
run = tmp_path / "run"
state.mkdir()
run.mkdir()
monkeypatch.setenv("MCQEMU_STATE_DIR", str(state))
monkeypatch.setenv("MCQEMU_RUNTIME_DIR", str(run))
return SimpleNamespace(state=state, run=run)
def write_registry(dirs, *records) -> None:
"""Pre-seed the registry file before a Client connects (lifespan loads it)."""
(dirs.state / "vms.json").write_text(json.dumps({r.name: r.to_dict() for r in records}))
@pytest.fixture
async def client(dirs):
async with Client(mcp) as c:
yield c
def result_data(result):
# Empty-list results carry no text content block; .data covers both shapes.
if result.data is not None:
return result.data
return json.loads(result.content[0].text)
class FakeQMPClient:
"""Stands in for qemu.qmp.QMPClient. Script it via class attributes:
responses: dict command -> canned reply, Exception to raise, or callable(args)
events_to_emit: list of event dicts fed to the events queue
connect_error: exception raised from connect()
calls: recorded (command, arguments) tuples
"""
responses: dict = {}
events_to_emit: list = []
connect_error: Exception | None = None
calls: list = []
@classmethod
def reset(cls):
cls.responses = {}
cls.events_to_emit = []
cls.connect_error = None
cls.calls = []
def __init__(self, name: str):
self.name = name
self.await_greeting = True
self.negotiate = True
self._events: asyncio.Queue = asyncio.Queue()
async def connect(self, address):
if FakeQMPClient.connect_error is not None:
raise FakeQMPClient.connect_error
for event in FakeQMPClient.events_to_emit:
self._events.put_nowait(event)
async def disconnect(self):
pass
async def execute(self, command, arguments=None):
FakeQMPClient.calls.append((command, arguments))
reply = FakeQMPClient.responses.get(command, {})
if isinstance(reply, Exception):
raise reply
if callable(reply):
return reply(arguments)
return reply
@property
def events(self):
return self._events
@pytest.fixture
def fake_qmp(monkeypatch):
FakeQMPClient.reset()
monkeypatch.setattr("mcqemu.qmp.QMPClient", FakeQMPClient)
monkeypatch.setattr("mcqemu.qga.QMPClient", FakeQMPClient)
return FakeQMPClient
@pytest.fixture
def all_pids_dead(monkeypatch):
monkeypatch.setattr("mcqemu.registry.pid_alive", lambda pid: False)
monkeypatch.setattr("mcqemu.tools.lifecycle.pid_alive", lambda pid: False)
@pytest.fixture
def all_pids_alive(monkeypatch):
monkeypatch.setattr("mcqemu.registry.pid_alive", lambda pid: bool(pid))
monkeypatch.setattr("mcqemu.tools.lifecycle.pid_alive", lambda pid: bool(pid))