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