mcp-arduino/tests/test_arduino_sketch.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

309 lines
11 KiB
Python

"""
Tests for ArduinoSketch component
"""
from unittest.mock import patch
import pytest
from tests.conftest import create_sketch_directory
class TestArduinoSketch:
"""Test suite for ArduinoSketch component"""
@pytest.mark.asyncio
async def test_create_sketch_success(self, sketch_component, test_context, temp_dir):
"""Test successful sketch creation"""
# Mock the file opening to prevent actual file opening during tests
with patch.object(sketch_component, '_open_file'):
result = await sketch_component.create_sketch(
ctx=test_context,
sketch_name="TestSketch"
)
assert result["success"] is True
assert "TestSketch" in result["message"]
# Verify sketch directory was created
sketch_dir = temp_dir / "sketches" / "TestSketch"
assert sketch_dir.exists()
# Verify .ino file was created with boilerplate
ino_file = sketch_dir / "TestSketch.ino"
assert ino_file.exists()
content = ino_file.read_text()
assert "void setup()" in content
assert "void loop()" in content
@pytest.mark.asyncio
async def test_create_sketch_already_exists(self, sketch_component, test_context, temp_dir):
"""Test creating a sketch that already exists"""
# Mock the file opening to prevent actual file opening during tests
with patch.object(sketch_component, '_open_file'):
# Create sketch first time
await sketch_component.create_sketch(test_context, "DuplicateSketch")
# Try to create again
result = await sketch_component.create_sketch(test_context, "DuplicateSketch")
assert "error" in result
assert "already exists" in result["error"]
@pytest.mark.asyncio
async def test_create_sketch_invalid_name(self, sketch_component, test_context):
"""Test creating sketch with invalid name"""
invalid_names = ["../hack", "sketch/name", "sketch\\name", ".", ".."]
for invalid_name in invalid_names:
result = await sketch_component.create_sketch(test_context, invalid_name)
assert "error" in result
assert "Invalid sketch name" in result["error"]
@pytest.mark.asyncio
async def test_list_sketches_empty(self, sketch_component, test_context):
"""Test listing sketches when none exist"""
result = await sketch_component.list_sketches(test_context)
assert "No Arduino sketches found" in result
@pytest.mark.asyncio
async def test_list_sketches_multiple(self, sketch_component, test_context, temp_dir):
"""Test listing multiple sketches"""
# Create several sketches
sketch_names = ["Blink", "Servo", "Temperature"]
for name in sketch_names:
create_sketch_directory(temp_dir / "sketches", name)
result = await sketch_component.list_sketches(test_context)
assert f"Found {len(sketch_names)} Arduino sketch(es)" in result
for name in sketch_names:
assert name in result
@pytest.mark.asyncio
async def test_read_sketch_success(self, sketch_component, test_context, temp_dir, sample_sketch_content):
"""Test reading sketch content"""
# Create a sketch
sketch_dir = create_sketch_directory(
temp_dir / "sketches",
"ReadTest",
sample_sketch_content
)
result = await sketch_component.read_sketch(
test_context,
"ReadTest"
)
assert result["success"] is True
assert result["content"] == sample_sketch_content
assert result["lines"] == len(sample_sketch_content.splitlines())
@pytest.mark.asyncio
async def test_read_sketch_not_found(self, sketch_component, test_context):
"""Test reading non-existent sketch"""
result = await sketch_component.read_sketch(
test_context,
"NonExistent"
)
assert "error" in result
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_write_sketch_new(self, sketch_component, test_context, temp_dir, sample_sketch_content):
"""Test writing a new sketch file"""
result = await sketch_component.write_sketch(
test_context,
"NewSketch",
sample_sketch_content,
auto_compile=False # Skip compilation for test
)
assert result["success"] is True
assert result["lines"] == len(sample_sketch_content.splitlines())
# Verify file was written
ino_file = temp_dir / "sketches" / "NewSketch" / "NewSketch.ino"
assert ino_file.exists()
assert ino_file.read_text() == sample_sketch_content
@pytest.mark.asyncio
async def test_write_sketch_update(self, sketch_component, test_context, temp_dir):
"""Test updating existing sketch"""
# Create initial sketch
sketch_dir = create_sketch_directory(
temp_dir / "sketches",
"UpdateTest",
"// Original content"
)
# Update with new content
new_content = "// Updated content\nvoid setup() {}\nvoid loop() {}"
result = await sketch_component.write_sketch(
test_context,
"UpdateTest",
new_content,
auto_compile=False
)
assert result["success"] is True
# Verify update
ino_file = sketch_dir / "UpdateTest.ino"
assert ino_file.read_text() == new_content
@pytest.mark.asyncio
async def test_compile_sketch_success(self, sketch_component, test_context, temp_dir, mock_arduino_cli):
"""Test successful sketch compilation"""
# Setup mock response
mock_arduino_cli.return_value.returncode = 0
mock_arduino_cli.return_value.stdout = "Compilation successful"
# Create sketch
create_sketch_directory(temp_dir / "sketches", "CompileTest")
result = await sketch_component.compile_sketch(
test_context,
"CompileTest"
)
assert result["success"] is True
assert "compiled successfully" in result["message"]
# Verify arduino-cli was called correctly
mock_arduino_cli.assert_called_once()
call_args = mock_arduino_cli.call_args[0][0]
assert "compile" in call_args
assert "--fqbn" in call_args
@pytest.mark.asyncio
async def test_compile_sketch_failure(self, sketch_component, test_context, temp_dir, mock_arduino_cli):
"""Test compilation failure"""
# Setup mock response
mock_arduino_cli.return_value.returncode = 1
mock_arduino_cli.return_value.stderr = "error: expected ';' before '}'"
create_sketch_directory(temp_dir / "sketches", "BadSketch")
result = await sketch_component.compile_sketch(
test_context,
"BadSketch"
)
assert "error" in result
assert "Compilation failed" in result["error"]
assert "expected ';'" in result["stderr"]
@pytest.mark.asyncio
async def test_upload_sketch_success(self, sketch_component, test_context, temp_dir, mock_arduino_cli):
"""Test successful sketch upload"""
# Setup mock response
mock_arduino_cli.return_value.returncode = 0
mock_arduino_cli.return_value.stdout = "Upload complete"
create_sketch_directory(temp_dir / "sketches", "UploadTest")
result = await sketch_component.upload_sketch(
test_context,
"UploadTest",
"/dev/ttyUSB0"
)
assert result["success"] is True
assert "uploaded successfully" in result["message"]
assert result["port"] == "/dev/ttyUSB0"
# Verify arduino-cli was called with upload
call_args = mock_arduino_cli.call_args[0][0]
assert "upload" in call_args
assert "--port" in call_args
assert "/dev/ttyUSB0" in call_args
@pytest.mark.asyncio
async def test_upload_sketch_port_error(self, sketch_component, test_context, temp_dir, mock_arduino_cli):
"""Test upload failure due to port issues"""
# Setup mock response
mock_arduino_cli.return_value.returncode = 1
mock_arduino_cli.return_value.stderr = "can't open device '/dev/ttyUSB0': Permission denied"
create_sketch_directory(temp_dir / "sketches", "PortTest")
result = await sketch_component.upload_sketch(
test_context,
"PortTest",
"/dev/ttyUSB0"
)
assert "error" in result
assert "Upload failed" in result["error"]
assert "Permission denied" in result["stderr"]
@pytest.mark.asyncio
async def test_write_with_auto_compile(self, sketch_component, test_context, temp_dir, mock_arduino_cli):
"""Test write with auto-compilation enabled"""
# Setup successful compilation
mock_arduino_cli.return_value.returncode = 0
mock_arduino_cli.return_value.stdout = "Compilation successful"
result = await sketch_component.write_sketch(
test_context,
"AutoCompile",
"void setup() {}\nvoid loop() {}",
auto_compile=True
)
assert result["success"] is True
assert "compilation" in result
# Verify compilation was triggered
mock_arduino_cli.assert_called_once()
call_args = mock_arduino_cli.call_args[0][0]
assert "compile" in call_args
@pytest.mark.asyncio
async def test_list_sketches_resource(self, sketch_component, temp_dir):
"""Test the MCP resource for listing sketches"""
# Create some sketches
create_sketch_directory(temp_dir / "sketches", "Resource1")
create_sketch_directory(temp_dir / "sketches", "Resource2")
# Call the resource method directly
result = await sketch_component.list_sketches_resource()
assert "Found 2 Arduino sketch(es)" in result
assert "Resource1" in result
assert "Resource2" in result
@pytest.mark.asyncio
async def test_read_additional_file(self, sketch_component, test_context, temp_dir):
"""Test reading additional files in sketch directory"""
# Create sketch with additional header file
sketch_dir = create_sketch_directory(temp_dir / "sketches", "MultiFile")
header_file = sketch_dir / "config.h"
header_content = "#define PIN_LED 13"
header_file.write_text(header_content)
result = await sketch_component.read_sketch(
test_context,
"MultiFile",
"config.h"
)
assert result["success"] is True
assert result["content"] == header_content
@pytest.mark.asyncio
async def test_write_disallowed_extension(self, sketch_component, test_context):
"""Test writing file with disallowed extension"""
result = await sketch_component.write_sketch(
test_context,
"BadExt",
"malicious content",
file_name="hack.exe",
auto_compile=False
)
assert "error" in result
assert "not allowed" in result["error"]