mcp-arduino/tests/conftest.py
Ryan Malloy eb524b8c1d Major project refactor: Rename to mcp-arduino with smart client capabilities
BREAKING CHANGES:
- Package renamed from mcp-arduino-server to mcp-arduino
- Command changed to 'mcp-arduino' (was 'mcp-arduino-server')
- Repository moved to git.supported.systems/MCP/mcp-arduino

NEW FEATURES:
 Smart client capability detection and dual-mode sampling support
 Intelligent WireViz templates with component-specific circuits (LED, motor, sensor, button, display)
 Client debug tools for MCP capability inspection
 Enhanced error handling with progressive enhancement patterns

IMPROVEMENTS:
🧹 Major repository cleanup - removed 14+ experimental files and tests
📝 Consolidated and reorganized documentation
🐛 Fixed import issues and applied comprehensive linting with ruff
📦 Updated author information to Ryan Malloy (ryan@supported.systems)
🔧 Fixed package version references in startup code

TECHNICAL DETAILS:
- Added dual-mode WireViz: AI generation for sampling clients, smart templates for others
- Implemented client capability detection via MCP handshake inspection
- Created progressive enhancement pattern for universal MCP client compatibility
- Organized test files into proper structure (tests/examples/)
- Applied comprehensive code formatting and lint fixes

The server now provides excellent functionality for ALL MCP clients regardless
of their sampling capabilities, while preserving advanced features for clients
that support them.

Version: 2025.09.27.1
2025-09-27 20:16:43 -06:00

252 lines
6.8 KiB
Python

"""
Pytest configuration and fixtures for mcp-arduino-server tests
"""
import tempfile
from collections.abc import Generator
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch
import pytest
from fastmcp import Context
from mcp_arduino_server.components import (
ArduinoBoard,
ArduinoDebug,
ArduinoLibrary,
ArduinoSketch,
WireViz,
)
from mcp_arduino_server.config import ArduinoServerConfig
@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for test files"""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def test_config(temp_dir: Path) -> ArduinoServerConfig:
"""Create a test configuration with temporary directories"""
config = ArduinoServerConfig(
sketches_base_dir=temp_dir / "sketches",
build_temp_dir=temp_dir / "build",
arduino_cli_path="arduino-cli", # Will be mocked
wireviz_path="wireviz", # Will be mocked
enable_client_sampling=True
)
# Ensure directories exist
config.sketches_base_dir.mkdir(parents=True, exist_ok=True)
config.build_temp_dir.mkdir(parents=True, exist_ok=True)
return config
@pytest.fixture
def mock_arduino_cli():
"""Mock arduino-cli subprocess calls"""
with patch('subprocess.run') as mock_run:
# Default successful response
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = '{"success": true}'
mock_run.return_value.stderr = ''
yield mock_run
@pytest.fixture
def mock_async_subprocess():
"""Mock async subprocess for components that use it"""
with patch('asyncio.create_subprocess_exec') as mock_exec:
mock_process = AsyncMock()
mock_process.returncode = 0
mock_process.stdout = AsyncMock()
mock_process.stderr = AsyncMock()
mock_process.stdin = AsyncMock()
# Mock readline for progress monitoring
mock_process.stdout.readline = AsyncMock(
side_effect=[
b'Downloading core...\n',
b'Installing core...\n',
b'Core installed successfully\n',
b'' # End of stream
]
)
mock_process.stderr.readline = AsyncMock(return_value=b'')
mock_process.wait = AsyncMock(return_value=0)
mock_process.communicate = AsyncMock(return_value=(b'Success', b''))
mock_exec.return_value = mock_process
yield mock_exec
@pytest.fixture
def test_context():
"""Create a test context with mocked elicitation support"""
# Create a mock context object
ctx = Mock(spec=Context)
# Add elicitation methods for interactive debugging tests
ctx.ask_user = AsyncMock(return_value="Continue to next breakpoint")
ctx.ask_confirmation = AsyncMock(return_value=True)
# Track progress and log calls for assertions
ctx.report_progress = AsyncMock()
ctx.info = AsyncMock()
ctx.debug = AsyncMock()
ctx.warning = AsyncMock()
ctx.error = AsyncMock()
# Add sampling support for AI features
ctx.sample = AsyncMock(return_value=Mock(
choices=[Mock(
message=Mock(
content="Generated YAML content"
)
)]
))
return ctx
@pytest.fixture
def sketch_component(test_config: ArduinoServerConfig) -> ArduinoSketch:
"""Create ArduinoSketch component instance"""
return ArduinoSketch(test_config)
@pytest.fixture
def library_component(test_config: ArduinoServerConfig) -> ArduinoLibrary:
"""Create ArduinoLibrary component instance"""
return ArduinoLibrary(test_config)
@pytest.fixture
def board_component(test_config: ArduinoServerConfig) -> ArduinoBoard:
"""Create ArduinoBoard component instance"""
return ArduinoBoard(test_config)
@pytest.fixture
def debug_component(test_config: ArduinoServerConfig) -> ArduinoDebug:
"""Create ArduinoDebug component instance"""
return ArduinoDebug(test_config)
@pytest.fixture
def wireviz_component(test_config: ArduinoServerConfig) -> WireViz:
"""Create WireViz component instance"""
return WireViz(test_config)
@pytest.fixture
def sample_sketch_content() -> str:
"""Sample Arduino sketch code"""
return """// Blink LED
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
"""
@pytest.fixture
def sample_wireviz_yaml() -> str:
"""Sample WireViz YAML configuration"""
return """
connectors:
Arduino:
type: Arduino Uno
pins: [GND, D13]
LED:
type: LED
pins: [cathode, anode]
cables:
jumper:
colors: [BK, RD]
connections:
- Arduino: [GND]
cable: [1]
LED: [cathode]
- Arduino: [D13]
cable: [2]
LED: [anode]
"""
@pytest.fixture
def mock_board_list_response() -> str:
"""Mock JSON response for board list"""
return """{
"detected_ports": [
{
"port": {
"address": "/dev/ttyUSB0",
"protocol": "serial",
"label": "Arduino Uno"
},
"matching_boards": [
{
"name": "Arduino Uno",
"fqbn": "arduino:avr:uno"
}
]
}
]
}"""
@pytest.fixture
def mock_library_search_response() -> str:
"""Mock JSON response for library search"""
return """{
"libraries": [
{
"name": "Servo",
"author": "Arduino",
"sentence": "Allows Arduino boards to control servo motors",
"paragraph": "This library can control a great number of servos.",
"category": "Device Control",
"architectures": ["*"],
"latest": {
"version": "1.1.8"
}
}
]
}"""
# Helper functions for testing
def create_sketch_directory(base_dir: Path, sketch_name: str, content: str = None) -> Path:
"""Helper to create a sketch directory with .ino file"""
sketch_dir = base_dir / sketch_name
sketch_dir.mkdir(parents=True, exist_ok=True)
ino_file = sketch_dir / f"{sketch_name}.ino"
if content is None:
content = f"// {sketch_name}\nvoid setup() {{}}\nvoid loop() {{}}"
ino_file.write_text(content)
return sketch_dir
def assert_progress_reported(ctx: Mock, min_calls: int = 1):
"""Assert that progress was reported at least min_calls times"""
assert ctx.report_progress.call_count >= min_calls, \
f"Expected at least {min_calls} progress reports, got {ctx.report_progress.call_count}"
def assert_logged_info(ctx: Mock, message_fragment: str):
"""Assert that an info message containing the fragment was logged"""
for call in ctx.info.call_args_list:
if message_fragment in str(call):
return
assert False, f"Info message containing '{message_fragment}' not found in logs"