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
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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
pip install -r requirements.txt 2>/dev/null || true
- name: Run logic tests
run: |
if [ -d tests ]; then python -m pytest tests/ -v --ignore=tests/test_example.py 2>/dev/null || 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()
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
158 changes: 158 additions & 0 deletions tests/test_inplants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Tests for in-plants Arduino firmware.

Re-implements key algorithms from inplants-argon.ino and inplants-xenon.ino:
- Moisture percentage calculation
- Battery voltage calculation
- Analog to percentage conversion
"""
import pytest


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, a, b):
"""Re-implementation of Arduino's constrain() function."""
return max(a, min(x, b))


def moisture_percentage(moisture_analog):
"""Re-implementation of moisture percentage calculation from .ino files.

Formula: 100 - (moisture_analog / 4095.00) * 100
This converts the raw analog reading to a moisture percentage.
Higher analog values = more moisture (lower resistance).
"""
return 100 - (moisture_analog / 4095.00) * 100


def battery_voltage(analog_value):
"""Re-implementation of battery voltage calculation.

Formula: analog_value * 0.0011224
Based on 12-bit ADC (4095 steps) with 3.3V reference.
"""
return analog_value * 0.0011224


def test_moisture_percentage_dry_sensor():
"""Test moisture percentage for dry sensor (high resistance, low analog value)."""
# Dry sensor reads ~0-200 analog
assert moisture_percentage(0) == 100.0
assert moisture_percentage(100) == pytest.approx(97.5555, rel=0.001)
assert moisture_percentage(200) == pytest.approx(95.1160, rel=0.001)


def test_moisture_percentage_wet_sensor():
"""Test moisture percentage for wet sensor (low resistance, high analog value)."""
# Wet sensor reads ~3500-4000 analog
assert moisture_percentage(3500) == pytest.approx(14.5299, rel=0.001)
assert moisture_percentage(3800) == pytest.approx(7.2044, rel=0.001)
assert moisture_percentage(4000) == pytest.approx(2.3201, rel=0.001)


def test_moisture_percentage_mid_range():
"""Test moisture percentage for mid-range values."""
# Mid-range around 2000-2500 analog
assert moisture_percentage(2000) == pytest.approx(51.1700, rel=0.001)
assert moisture_percentage(2500) == pytest.approx(38.9499, rel=0.001)


def test_moisture_percentage_edge_cases():
"""Test edge cases for moisture calculation."""
# Maximum analog value (saturated)
assert moisture_percentage(4095) == 0.0
# Minimum analog value (completely dry)
assert moisture_percentage(0) == 100.0


def test_battery_voltage_calculation():
"""Test battery voltage calculation from ADC reading."""
# 0 ADC = 0V
assert battery_voltage(0) == 0.0
# Mid-range: 1838 * 0.0011224 ≈ 2.06V
assert battery_voltage(1838) == pytest.approx(2.0622, rel=0.001)
# Full scale: 4095 * 0.0011224 ≈ 4.59V
assert battery_voltage(4095) == pytest.approx(4.5970, rel=0.001)


def test_battery_voltage_thresholds():
"""Test battery voltage calculations for common thresholds."""
# 3.3V threshold corresponds to ~2940 ADC
assert battery_voltage(2940) == pytest.approx(3.2999, rel=0.001)
# 3.7V (typical LiPo nominal) corresponds to ~3297 ADC
assert battery_voltage(3297) == pytest.approx(3.6985, rel=0.001)
# 4.2V (full charge) corresponds to ~3742 ADC
assert battery_voltage(3742) == pytest.approx(4.1995, rel=0.001)


def test_voltage_to_percentage_conversion():
"""Test converting battery voltage to percentage using Arduino map."""
# Map battery voltage from 3.0V-4.2V to 0-100%
# 3.0V = 0%, 4.2V = 100%

# 3.0V (2671 ADC) = 0%
voltage_3v = battery_voltage(2671)
percent_3v = arduino_map(int(voltage_3v * 1000), 3000, 4200, 0, 100)
assert percent_3v == 0

# 4.2V (3742 ADC) = 100%
voltage_4v = battery_voltage(3742)
percent_4v = arduino_map(int(voltage_4v * 1000), 3000, 4200, 0, 100)
assert percent_4v == 100

# 3.6V (3207 ADC) = 49% (due to integer math in map())
voltage_36v = battery_voltage(3207)
percent_36v = arduino_map(int(voltage_36v * 1000), 3000, 4200, 0, 100)
assert percent_36v == 49


def test_moisture_percentage_clamping():
"""Test that moisture percentage stays within valid bounds."""
# Test with values outside normal range
assert moisture_percentage(-100) == pytest.approx(102.4420, rel=0.001)
assert moisture_percentage(5000) == pytest.approx(-22.1001, rel=0.001)


def test_arduino_map_helper():
"""Test the Arduino map() re-implementation."""
assert arduino_map(512, 0, 1023, 0, 255) == 127
assert arduino_map(0, 0, 1023, 0, 255) == 0
assert arduino_map(1023, 0, 1023, 0, 255) == 255
assert arduino_map(200, 0, 4095, 0, 100) == 4


def test_arduino_constrain_helper():
"""Test the Arduino constrain() re-implementation."""
assert arduino_constrain(5, 0, 10) == 5
assert arduino_constrain(-5, 0, 10) == 0
assert arduino_constrain(15, 0, 10) == 10


def test_combined_moisture_and_battery_logic():
"""Test combined moisture and battery calculations."""
# Simulate a reading scenario
moisture_analog = 2500
battery_analog = 3200

moisture_pct = moisture_percentage(moisture_analog)
battery_v = battery_voltage(battery_analog)

# Verify moisture is around 39%
assert moisture_pct == pytest.approx(38.9499, rel=0.001)

# Verify battery is around 3.59V
assert battery_v == pytest.approx(3.5917, rel=0.001)


def test_sensor_calibration_values():
"""Test with typical sensor calibration values."""
# Dry air reading (typical): ~800-1000
assert moisture_percentage(800) == pytest.approx(80.4884, rel=0.001)
assert moisture_percentage(1000) == pytest.approx(75.5555, rel=0.001)

# Water reading (typical): ~2000-2500
assert moisture_percentage(2000) == pytest.approx(51.1700, rel=0.001)
assert moisture_percentage(2200) == pytest.approx(46.2759, rel=0.001)
Loading