Test infrastructure with conftest fixtures mocking run_shell_args/run_adb for device-free testing across all 8 mixins. Fixed: UI parser regex couldn't match hyphenated XML attributes (content-desc, resource-id). Notification parser captured trailing parenthesis in package names.
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
"""Shared test fixtures for mcadb tests."""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from src.models import CommandResult
|
|
from src.server import ADBServer
|
|
|
|
# --- Helpers ---
|
|
|
|
|
|
def ok(stdout: str = "", stderr: str = "") -> CommandResult:
|
|
"""Create a successful CommandResult."""
|
|
return CommandResult(success=True, stdout=stdout, stderr=stderr, returncode=0)
|
|
|
|
|
|
def fail(stderr: str = "error", stdout: str = "") -> CommandResult:
|
|
"""Create a failed CommandResult."""
|
|
return CommandResult(success=False, stdout=stdout, stderr=stderr, returncode=1)
|
|
|
|
|
|
# --- Mock Context ---
|
|
|
|
|
|
@dataclass
|
|
class ElicitResult:
|
|
"""Minimal stand-in for FastMCP's ElicitationResult."""
|
|
|
|
action: str = "accept"
|
|
content: str = ""
|
|
|
|
|
|
@dataclass
|
|
class MockContext:
|
|
"""Mock MCP Context that records calls for assertion."""
|
|
|
|
messages: list[tuple[str, str]] = field(default_factory=list)
|
|
_elicit_response: ElicitResult = field(default_factory=ElicitResult)
|
|
|
|
async def info(self, msg: str) -> None:
|
|
self.messages.append(("info", msg))
|
|
|
|
async def warning(self, msg: str) -> None:
|
|
self.messages.append(("warning", msg))
|
|
|
|
async def error(self, msg: str) -> None:
|
|
self.messages.append(("error", msg))
|
|
|
|
async def elicit(self, msg: str, options: list[str] | None = None) -> ElicitResult:
|
|
self.messages.append(("elicit", msg))
|
|
return self._elicit_response
|
|
|
|
def set_elicit(self, action: str = "accept", content: str = "") -> None:
|
|
"""Configure the next elicit response."""
|
|
self._elicit_response = ElicitResult(action=action, content=content)
|
|
|
|
|
|
# --- Fixtures ---
|
|
|
|
|
|
def _reset_config(monkeypatch: pytest.MonkeyPatch, config_dir: Any) -> None:
|
|
"""Reset the Config singleton and point it at a temp directory.
|
|
|
|
CONFIG_DIR and CONFIG_FILE are module-level variables computed at
|
|
import time, so setting the env var isn't enough — we must patch
|
|
the variables directly.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
config_path = Path(config_dir)
|
|
monkeypatch.setattr("src.config.Config._instance", None)
|
|
monkeypatch.setattr("src.config.CONFIG_DIR", config_path)
|
|
monkeypatch.setattr("src.config.CONFIG_FILE", config_path / "config.json")
|
|
|
|
|
|
@pytest.fixture
|
|
def server() -> ADBServer:
|
|
"""Create an ADBServer with mocked ADB execution.
|
|
|
|
Both run_adb and run_shell_args are replaced with AsyncMock,
|
|
so no real subprocess calls are made. Configure return values
|
|
per-test with server.run_adb.return_value = ok("...").
|
|
"""
|
|
s = ADBServer()
|
|
s.run_adb = AsyncMock(return_value=ok()) # type: ignore[method-assign]
|
|
s.run_shell_args = AsyncMock(return_value=ok()) # type: ignore[method-assign]
|
|
s.run_shell = AsyncMock(return_value=ok()) # type: ignore[method-assign]
|
|
s.get_device_property = AsyncMock(return_value=None) # type: ignore[method-assign]
|
|
return s
|
|
|
|
|
|
@pytest.fixture
|
|
def ctx() -> MockContext:
|
|
"""Create a mock MCP Context."""
|
|
return MockContext()
|
|
|
|
|
|
@pytest.fixture
|
|
def _dev_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|
"""Enable developer mode for the test."""
|
|
_reset_config(monkeypatch, tmp_path / "dev-config")
|
|
from src.config import get_config
|
|
|
|
config = get_config()
|
|
config._settings["developer_mode"] = True
|
|
|
|
|
|
@pytest.fixture
|
|
def _no_dev_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|
"""Disable developer mode for the test."""
|
|
_reset_config(monkeypatch, tmp_path / "nodev-config")
|
|
from src.config import get_config
|
|
|
|
config = get_config()
|
|
config._settings["developer_mode"] = False
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|
"""Isolate config to a temp directory so tests don't touch real config."""
|
|
_reset_config(monkeypatch, tmp_path / "config")
|