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()
56 changes: 56 additions & 0 deletions tests/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""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 those algorithms in Python in your test file
3. Write pytest tests that verify the Python re-implementation

The arduino fixture provides constants (HIGH/LOW/INPUT/OUTPUT) and helpers
(map_value, constrain) matching Arduino's built-in functions.
"""
import pytest


@pytest.fixture
def arduino_constants():
"""Provides Arduino-like constants and helpers for testing re-implemented logic."""
class ArduinoConstants:
HIGH = 1
LOW = 0
INPUT = 0
OUTPUT = 1

@staticmethod
def map_value(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)

@staticmethod
def constrain(x, a, b):
"""Re-implementation of Arduino's constrain() function."""
return max(a, min(x, b))

return ArduinoConstants()


# Example test showing how to use the arduino fixture
def test_map_value_example(arduino_constants):
"""Test the map_value helper from the arduino fixture."""
# Map 512 from 0-1023 range to 0-255 range
assert arduino_constants.map_value(512, 0, 1023, 0, 255) == 127


def test_constrain_example(arduino_constants):
"""Test the constrain helper from the arduino fixture."""
# Constrain 150 to 0-100 range
assert arduino_constants.constrain(150, 0, 100) == 100
# Constrain -10 to 0-100 range
assert arduino_constants.constrain(-10, 0, 100) == 0
# Constrain 50 to 0-100 range
assert arduino_constants.constrain(50, 0, 100) == 50


# 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
78 changes: 78 additions & 0 deletions tests/test_moisture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Test moisture calculation logic from inplants-xenon.ino and inplants-argon.ino."""
import pytest


def moisture_percentage_from_analog(analog_value):
"""Re-implementation of the moisture calculation from the Arduino code.

moisture_percentage = (100 - ( (moisture_analog/4095.00) * 100 ) )
"""
return 100 - ((analog_value / 4095.00) * 100)


def test_moisture_percentage_empty_water():
"""When sensor is in air (dry), analog reading should be high, percentage low."""
# In air, sensor reads close to 4095
assert abs(moisture_percentage_from_analog(4095) - 0) < 0.01


def test_moisture_percentage_full_water():
"""When sensor is fully submerged, analog reading should be low, percentage high."""
# In water, sensor reads close to 0
assert abs(moisture_percentage_from_analog(0) - 100) < 0.01


def test_moisture_percentage_mid_range():
"""Test mid-range moisture values."""
# At ~2047 (mid-range), percentage should be ~50%
assert abs(moisture_percentage_from_analog(2047) - 50.01) < 0.1


def test_moisture_percentage_example_values():
"""Test with some example sensor values."""
# Dry soil - high analog value
assert 0 <= moisture_percentage_from_analog(3500) <= 15

# Moist soil - mid analog value
assert 40 <= moisture_percentage_from_analog(2000) <= 60

# Wet soil - low analog value
assert 80 <= moisture_percentage_from_analog(500) <= 100


def test_moisture_percentage_edge_cases():
"""Test edge cases for moisture calculation."""
# Minimum valid analog value
assert abs(moisture_percentage_from_analog(0) - 100) < 0.01

# Maximum valid analog value (12-bit ADC max)
assert abs(moisture_percentage_from_analog(4095) - 0) < 0.01


def test_voltage_calculation():
"""Test voltage calculation from analog reading.

From xenon code: float voltage = analogRead(BATT) * 0.0011224;
This appears to be for a 12-bit ADC with reference voltage calculation.
"""
# If analogRead returns 4095 (max), voltage should be ~4.6V (4095 * 0.0011224)
max_voltage = 4095 * 0.0011224
assert abs(max_voltage - 4.598) < 0.01

# If analogRead returns 0, voltage should be 0
assert 4095 * 0.0011224 > 0


def test_battery_threshold_logic():
"""Test the battery threshold logic from the Xenon code.

if(BATT > 1){
// Battery powered mode - publish every 2 minutes and sleep
} else {
// USB powered mode - publish every 10 minutes
}
"""
# When BATT > 1, device is on battery
assert 2 > 1 # Battery mode
assert not (1 > 1) # Not battery mode (at threshold, 1 is not > 1)
assert not (0 > 1) # USB mode (0 is not > 1)
Loading