Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/compile-sketches@v1
with:
sketch-paths: |
- .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
- name: Run logic tests
run: |
if [ -d tests ]; then python -m pytest tests/ -v; fi
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
.DS_Store
.DS_Store
# Auto-added by Marisol pipeline
node_modules/
__pycache__/
*.pyc
.pytest_cache/
*.o
*.so
.env
debug_*.py
.cache/
dist/
build/
*.egg-info/
45 changes: 45 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Auto-generated conftest.py -- stubs for Arduino/embedded C testing.

Strategy: Arduino .ino/.cpp files can't be imported in Python, so we test by
re-implementing the core algorithms (state machines, math, protocol encoding,
parsers) in Python and verifying them with pytest. The conftest provides mock
serial and common helper dependencies.
"""
import sys
import os
import pytest
from unittest.mock import MagicMock, patch

# Add repo root to path so test files can import source modules
sys.path.insert(0, '')

# Mock common Arduino Python helper dependencies
for mod_name in ['serial', 'serial.tools', 'serial.tools.list_ports',
'pyserial', 'pyfirmata', 'pyfirmata2']:
sys.modules[mod_name] = MagicMock()


class ArduinoConstants:
"""Provides Arduino-style constants for Python re-implementations."""
HIGH = 1
LOW = 0
INPUT = 0
OUTPUT = 1
INPUT_PULLUP = 2
A0, A1, A2, A3, A4, A5 = range(14, 20)

@staticmethod
def map_value(x, in_min, in_max, out_min, out_max):
"""Arduino map() function."""
return int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)

@staticmethod
def constrain(x, a, b):
"""Arduino constrain() function."""
return max(a, min(x, b))


@pytest.fixture
def arduino():
"""Provides Arduino-like constants and helpers for testing re-implemented logic."""
return ArduinoConstants()
226 changes: 226 additions & 0 deletions tests/embedded_mocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"""Mock libraries for embedded C testing."""


def arduino_map(x, in_min, in_max, out_min, out_max):
"""Re-implementation of Arduino's map() function."""
return int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)


def arduino_constrain(x, low, high):
"""Re-implementation of Arduino's constrain() function."""
return max(low, min(x, high))


def analog_to_voltage(analog_value, bits=10, ref=3.3):
"""Convert analog reading to voltage."""
max_value = (1 << bits) - 1
return analog_value * (ref / max_value)


class MockI2C:
"""Mock I2C bus."""
def __init__(self, sda=21, scl=22, frequency=100000):
self.sda = sda
self.scl = scl
self.frequency = frequency
self._tx_buffer = b""
self._rx_buffer = b""

def write(self, data):
self._tx_buffer = data

def read(self, n):
return self._rx_buffer[:n]


class MockSPI:
"""Mock SPI bus."""
def __init__(self, sck=18, miso=19, mosi=23, cs=5):
self.sck = sck
self.miso = miso
self.mosi = mosi
self.cs = cs

def transfer(self, data):
return data


class MockUART:
"""Mock UART serial port."""
def __init__(self, baudrate=115200, rx_pin=3, tx_pin=1):
self.baudrate = baudrate
self.rx_pin = rx_pin
self.tx_pin = tx_pin
self._tx_log = b""
self._rx_buffer = b""

def write(self, data):
self._tx_log += data

def read(self, n=-1):
if n < 0:
result = self._rx_buffer
self._rx_buffer = b""
return result
result = self._rx_buffer[:n]
self._rx_buffer = self._rx_buffer[n:]
return result

def readline(self):
if b"\n" in self._rx_buffer:
idx = self._rx_buffer.index(b"\n") + 1
result = self._rx_buffer[:idx]
self._rx_buffer = self._rx_buffer[idx:]
return result
return b""

def inject_rx(self, data):
self._rx_buffer += data


class MockGPIO:
"""Mock GPIO pin."""
def __init__(self, pin, mode="OUTPUT"):
self.pin = pin
self.mode = mode
self.value = 0

def high(self):
self.value = 1

def low(self):
self.value = 0

def read(self):
return self.value


class MockNeoPixel:
"""Mock NeoPixel LED strip."""
def __init__(self, pin, n, brightness=0.5):
self.pin = pin
self.n = n
self.brightness = brightness
self.pixels = [(0, 0, 0)] * n

def set_pixel(self, i, r, g, b):
if 0 <= i < self.n:
self.pixels[i] = (r, g, b)

def show(self):
pass


class MockWiFi:
"""Mock WiFi connection."""
def __init__(self):
self.connected = False
self.ssid = None

def connect(self, ssid, password):
self.ssid = ssid
self.connected = True

def disconnect(self):
self.connected = False


class MockBLE:
"""Mock BLE connection."""
def __init__(self):
self.connected = False

def connect(self):
self.connected = True

def disconnect(self):
self.connected = False


class MockPreferences:
"""Mock non-volatile storage."""
def __init__(self):
self._data = {}

def put(self, key, value):
self._data[key] = value

def get(self, key, default=None):
return self._data.get(key, default)


class MockSSD1306:
"""Mock SSD1306 OLED display."""
def __init__(self, width=128, height=64):
self.width = width
self.height = height
self.buffer = [[0] * width for _ in range(height)]

def pixel(self, x, y, color=1):
if 0 <= x < self.width and 0 <= y < self.height:
self.buffer[y][x] = color


class MockHT16K33:
"""Mock HT16K33 LED driver."""
def __init__(self, address=0x70):
self.address = address
self.leds = [0] * 16

def set_led(self, pin, state):
if 0 <= pin < 16:
self.leds[pin] = 1 if state else 0


class MockRotaryEncoder:
"""Mock rotary encoder."""
def __init__(self, pin_a=2, pin_b=3):
self.pin_a = pin_a
self.pin_b = pin_b
self.position = 0

def update(self):
pass


class MockADC:
"""Mock ADC channel."""
def __init__(self, pin, bits=12):
self.pin = pin
self.bits = bits
self.value = 0

def read(self):
return self.value


class MockPWM:
"""Mock PWM channel."""
def __init__(self, pin, frequency=50):
self.pin = pin
self.frequency = frequency
self.duty_cycle = 0

def write(self, value):
self.duty_cycle = value


class MockTemperatureSensor:
"""Mock temperature sensor."""
def __init__(self, pin=0):
self.pin = pin
self.temperature = 25.0

def read_temperature(self):
return self.temperature


class MockAccelerometer:
"""Mock accelerometer."""
def __init__(self):
self.x = 0
self.y = 0
self.z = 1.0 # 1G default

def read_acceleration(self):
return self.x, self.y, self.z
55 changes: 55 additions & 0 deletions tests/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""test_example.py — Starter template for Arduino project tests.

Arduino .ino/.cpp files CANNOT be imported in Python. Instead:
1. READ the source files to understand the algorithms
2. RE-IMPLEMENT the logic in Python in this test file
3. Test the Python re-implementation with pytest

The `arduino` fixture provides constants (HIGH, LOW, A0-A5) and helpers
(map_value, constrain). Also see embedded_mocks.py for I2C/SPI/UART mocks.
DO NOT modify conftest.py.
"""
import pytest
from embedded_mocks import (
MockI2C, MockSPI, MockUART, MockGPIO, MockNeoPixel,
MockWiFi, MockBLE, MockPreferences,
arduino_map, arduino_constrain, analog_to_voltage,
)


def test_arduino_map():
"""Test Arduino map() function re-implementation."""
assert arduino_map(0, 0, 1023, 0, 255) == 0
assert arduino_map(512, 0, 1023, 0, 255) == 127
assert arduino_map(1023, 0, 1023, 0, 255) == 255


def test_arduino_constrain():
"""Test Arduino constrain() function."""
assert arduino_constrain(150, 0, 100) == 100
assert arduino_constrain(-10, 0, 100) == 0
assert arduino_constrain(50, 0, 100) == 50


def test_analog_voltage_conversion():
"""Test ADC raw value to voltage conversion."""
assert abs(analog_to_voltage(512, bits=10, ref=3.3) - 1.65) < 0.01
assert abs(analog_to_voltage(0, bits=10) - 0.0) < 0.01
assert abs(analog_to_voltage(1023, bits=10, ref=3.3) - 3.3) < 0.01


def test_serial_protocol_example():
"""Example: test a serial command protocol."""
uart = MockUART(baudrate=115200)
# Simulate sending a command
uart.write(b"AT+CMD\r\n")
assert uart._tx_log == b"AT+CMD\r\n"
# Simulate receiving a response
uart.inject_rx(b"OK\r\n")
assert uart.readline() == b"OK\r\n"


# TODO: Read the .ino source files and re-implement key algorithms below:
# def test_state_machine():
# """Re-implement the state machine from the Arduino source."""
# pass
Loading
Loading