mcp-adb/tests/conftest.py
Ryan Malloy fb297f7937 Add pytest suite (216 tests) and fix UI/notification parser bugs
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.
2026-02-11 03:38:37 -07:00

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")