110 lines
3.1 KiB
Python
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))
|