Add mock Arduino for x86_64 host-side testing

Complete Arduino API mock (mock_arduino.h/cpp) enabling application
code to compile and run on PC without hardware. Includes MockSerial,
String class, GPIO/analog/timing/interrupt mocks with state tracking
and test control API.

- Arduino.h, Wire.h, SPI.h shims intercept includes in test builds
- System test template (test_system.cpp) using SimHal
- CMakeLists.txt builds mock_arduino as static lib, links both suites
- Root test.sh/test.bat with --unit/--system/--clean/--verbose flags
- test.bat auto-detects MSVC via vswhere + vcvarsall.bat
- Doctor reports nuanced compiler status (on PATH vs installed)
- Refresh pulls mock infrastructure into existing projects
- 15 tests passing: 7 unit (MockHal) + 8 system (SimHal)
This commit is contained in:
Eric Ratliff
2026-02-20 08:21:11 -06:00
parent 1ae136530f
commit aa1e9d5043
13 changed files with 1831 additions and 16 deletions

View File

@@ -13,6 +13,7 @@ pub struct SystemHealth {
pub dialout_ok: bool, pub dialout_ok: bool,
pub cmake_ok: bool, pub cmake_ok: bool,
pub cpp_compiler_ok: bool, pub cpp_compiler_ok: bool,
pub cpp_on_path: bool,
pub git_ok: bool, pub git_ok: bool,
pub ports_found: usize, pub ports_found: usize,
} }
@@ -99,7 +100,14 @@ pub fn check_system_health() -> SystemHealth {
let cmake_ok = which::which("cmake").is_ok(); let cmake_ok = which::which("cmake").is_ok();
// C++ compiler (optional -- for host tests) // C++ compiler (optional -- for host tests)
let cpp_compiler_ok = has_cpp_compiler(); let cpp_on_path = which::which("g++").is_ok()
|| which::which("clang++").is_ok()
|| which::which("cl").is_ok();
let cpp_compiler_ok = if cpp_on_path {
true
} else {
has_cpp_compiler()
};
// git // git
let git_ok = which::which("git").is_ok(); let git_ok = which::which("git").is_ok();
@@ -115,6 +123,7 @@ pub fn check_system_health() -> SystemHealth {
dialout_ok, dialout_ok,
cmake_ok, cmake_ok,
cpp_compiler_ok, cpp_compiler_ok,
cpp_on_path,
git_ok, git_ok,
ports_found, ports_found,
} }
@@ -130,6 +139,20 @@ fn has_cpp_compiler() -> bool {
if which::which("cl").is_ok() { if which::which("cl").is_ok() {
return true; return true;
} }
// cl.exe may be installed via VS Build Tools but not on PATH.
// Check via vswhere.exe (ships with VS installer).
if let Ok(output) = std::process::Command::new(
r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe",
)
.args(["-latest", "-property", "installationPath"])
.output()
{
let path = String::from_utf8_lossy(&output.stdout);
let vcvarsall = format!("{}\\VC\\Auxiliary\\Build\\vcvarsall.bat", path.trim());
if std::path::Path::new(&vcvarsall).exists() {
return true;
}
}
} }
false false
} }
@@ -217,8 +240,14 @@ fn print_diagnostics(health: &SystemHealth) {
} }
// C++ compiler // C++ compiler
if health.cpp_compiler_ok { if health.cpp_on_path {
println!(" {} C++ compiler", "ok".green()); println!(" {} C++ compiler", "ok".green());
} else if health.cpp_compiler_ok {
println!(
" {} C++ compiler {}",
"ok".green(),
"(MSVC installed; test.bat will configure automatically)".bright_black()
);
} else { } else {
println!( println!(
" {} C++ compiler {}", " {} C++ compiler {}",
@@ -655,16 +684,16 @@ fn fix_spec_cmake(pm: Option<&str>) -> FixSpec {
fn fix_spec_cpp(pm: Option<&str>) -> FixSpec { fn fix_spec_cpp(pm: Option<&str>) -> FixSpec {
match pm { match pm {
Some("winget") => FixSpec::Auto {
prompt: "Install Visual Studio Build Tools via winget?",
program: "winget",
args: &["install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e"],
},
Some("choco") => FixSpec::Auto { Some("choco") => FixSpec::Auto {
prompt: "Install MinGW g++ via Chocolatey?", prompt: "Install MinGW g++ via Chocolatey? (recommended -- lands on PATH)",
program: "choco", program: "choco",
args: &["install", "mingw", "-y"], args: &["install", "mingw", "-y"],
}, },
Some("winget") => FixSpec::Auto {
prompt: "Install Visual Studio Build Tools via winget? (test.bat will find it automatically)",
program: "winget",
args: &["install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e"],
},
Some("brew") => FixSpec::Manual { Some("brew") => FixSpec::Manual {
message: "run: xcode-select --install", message: "run: xcode-select --install",
}, },
@@ -751,7 +780,7 @@ fn hint_cmake() -> &'static str {
fn hint_cpp_compiler() -> &'static str { fn hint_cpp_compiler() -> &'static str {
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
"install: winget install Microsoft.VisualStudio.2022.BuildTools (or MinGW g++)" "install: choco install mingw (or open Developer Command Prompt for MSVC)"
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
"install: xcode-select --install" "install: xcode-select --install"
} else { } else {

View File

@@ -16,10 +16,20 @@ const REFRESHABLE_FILES: &[&str] = &[
"upload.bat", "upload.bat",
"monitor.sh", "monitor.sh",
"monitor.bat", "monitor.bat",
"test.sh",
"test.bat",
"_detect_port.ps1", "_detect_port.ps1",
"_monitor_filter.ps1", "_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.sh",
"test/run_tests.bat", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
]; ];
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> { pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {

158
templates/basic/test.bat Normal file
View File

@@ -0,0 +1,158 @@
@echo off
setlocal enabledelayedexpansion
:: =========================================================================
:: test.bat -- Build and run all host-side tests (no Arduino board needed)
::
:: Usage:
:: test Build and run all tests
:: test --clean Clean rebuild
:: test --verbose Verbose test output
:: test --unit Run only unit tests
:: test --system Run only system tests
::
:: This script builds your application against mock_arduino (a fake Arduino
:: environment) and runs Google Test suites. Your app code compiles and runs
:: on your PC -- no board, no wires, no USB.
::
:: Prerequisites:
:: cmake >= 3.14, a C++ compiler (g++, clang++, or MSVC), git
::
:: First run downloads Google Test automatically (~30 seconds).
:: =========================================================================
set "SCRIPT_DIR=%~dp0"
:: %~dp0 always ends with \ which breaks cmake quoting
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "TEST_DIR=%SCRIPT_DIR%\test"
set "BUILD_DIR=%TEST_DIR%\build"
set DO_CLEAN=0
set VERBOSE=
set FILTER=
:parse_args
if "%1"=="" goto :done_args
if "%1"=="--clean" ( set DO_CLEAN=1 & shift & goto :parse_args )
if "%1"=="--verbose" ( set VERBOSE=--verbose & shift & goto :parse_args )
if "%1"=="--unit" ( set "FILTER=-R test_unit" & shift & goto :parse_args )
if "%1"=="--system" ( set "FILTER=-R test_system" & shift & goto :parse_args )
if "%1"=="--help" goto :show_help
if "%1"=="-h" goto :show_help
echo [FAIL] Unknown option: %1
echo Try: test --help
exit /b 1
:done_args
:: -- Check prerequisites ---------------------------------------------------
where cmake >nul 2>&1
if errorlevel 1 (
echo [FAIL] cmake not found.
echo Install: winget install Kitware.CMake
echo Or run: anvil doctor --fix
exit /b 1
)
set "HAS_COMPILER=0"
where g++ >nul 2>&1 && set "HAS_COMPILER=1"
where clang++ >nul 2>&1 && set "HAS_COMPILER=1"
where cl >nul 2>&1 && set "HAS_COMPILER=1"
if "%HAS_COMPILER%"=="1" goto :compiler_ok
:: cl.exe not on PATH -- try to find VS Build Tools via vswhere
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" goto :no_compiler
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do (
set "VS_PATH=%%i"
)
if not defined VS_PATH goto :no_compiler
set "VCVARS=!VS_PATH!\VC\Auxiliary\Build\vcvarsall.bat"
if not exist "!VCVARS!" goto :no_compiler
echo [TEST] Setting up MSVC environment...
call "!VCVARS!" x64 >nul 2>&1
where cl >nul 2>&1
if errorlevel 1 goto :no_compiler
set "HAS_COMPILER=1"
goto :compiler_ok
:no_compiler
echo [FAIL] No C++ compiler found.
echo.
echo Easiest fix -- install MinGW g++ ^(just works, lands on PATH^):
echo choco install mingw -y
echo.
echo Or if you have VS Build Tools, open "Developer Command Prompt"
echo and run test.bat from there.
echo.
echo Or run: anvil doctor --fix
exit /b 1
:compiler_ok
where git >nul 2>&1
if errorlevel 1 (
echo [FAIL] git not found ^(needed to fetch Google Test^).
echo Install: winget install Git.Git
exit /b 1
)
:: -- Build ------------------------------------------------------------------
if %DO_CLEAN%==1 (
if exist "%BUILD_DIR%" (
echo [TEST] Cleaning build directory...
rmdir /s /q "%BUILD_DIR%"
)
)
if not exist "%BUILD_DIR%\CMakeCache.txt" (
echo [TEST] Configuring test build. First run will fetch Google Test...
cmake -S "%TEST_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Debug
if errorlevel 1 (
echo [FAIL] cmake configure failed.
echo Run 'anvil doctor' to check your environment.
exit /b 1
)
echo.
)
echo [TEST] Building tests...
cmake --build "%BUILD_DIR%" --parallel
if errorlevel 1 (
echo [FAIL] Build failed.
exit /b 1
)
echo.
echo [TEST] Running tests...
echo.
:: -- Run --------------------------------------------------------------------
set "CTEST_ARGS=--test-dir "%BUILD_DIR%" --output-on-failure"
if not "%VERBOSE%"=="" set "CTEST_ARGS=%CTEST_ARGS% %VERBOSE%"
if not "%FILTER%"=="" set "CTEST_ARGS=%CTEST_ARGS% %FILTER%"
ctest %CTEST_ARGS%
if errorlevel 1 (
echo.
echo [FAIL] Some tests failed.
exit /b 1
)
echo.
echo [PASS] All tests passed.
exit /b 0
:show_help
echo Usage: test [--clean] [--verbose] [--unit^|--system]
echo.
echo --clean Delete build cache and rebuild from scratch
echo --verbose Show individual test names and output
echo --unit Run only unit tests (Google Mock)
echo --system Run only system tests (SimHal)
exit /b 0

124
templates/basic/test.sh Normal file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env bash
#
# test.sh -- Build and run all host-side tests (no Arduino board needed)
#
# Usage:
# ./test.sh Build and run all tests
# ./test.sh --clean Clean rebuild
# ./test.sh --verbose Verbose test output
# ./test.sh --unit Run only unit tests
# ./test.sh --system Run only system tests
#
# This script builds your application against mock_arduino (a fake Arduino
# environment) and runs Google Test suites. Your app code compiles and runs
# on your PC -- no board, no wires, no USB.
#
# Prerequisites:
# cmake >= 3.14, a C++ compiler (g++, clang++, or MSVC), git
#
# First run downloads Google Test automatically (~30 seconds).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEST_DIR="$SCRIPT_DIR/test"
BUILD_DIR="$TEST_DIR/build"
# Color output
if [[ -t 1 ]]; then
RED=$'\033[0;31m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'
YLW=$'\033[0;33m'; BLD=$'\033[1m'; RST=$'\033[0m'
else
RED=''; GRN=''; CYN=''; YLW=''; BLD=''; RST=''
fi
info() { echo -e "${CYN}[TEST]${RST} $*"; }
ok() { echo -e "${GRN}[PASS]${RST} $*"; }
warn() { echo -e "${YLW}[WARN]${RST} $*"; }
die() { echo -e "${RED}[FAIL]${RST} $*" >&2; exit 1; }
DO_CLEAN=0
VERBOSE=""
FILTER=""
for arg in "$@"; do
case "$arg" in
--clean) DO_CLEAN=1 ;;
--verbose) VERBOSE="--verbose" ;;
--unit) FILTER="-R test_unit" ;;
--system) FILTER="-R test_system" ;;
-h|--help)
echo "Usage: ./test.sh [--clean] [--verbose] [--unit|--system]"
echo ""
echo " --clean Delete build cache and rebuild from scratch"
echo " --verbose Show individual test names and output"
echo " --unit Run only unit tests (Google Mock)"
echo " --system Run only system tests (SimHal)"
exit 0
;;
*) die "Unknown option: $arg (try --help)" ;;
esac
done
# -- Check prerequisites ----------------------------------------------------
if ! command -v cmake &>/dev/null; then
die "cmake not found."
echo " Install:" >&2
if [[ "$(uname)" == "Darwin" ]]; then
echo " brew install cmake" >&2
else
echo " sudo apt install cmake (Debian/Ubuntu)" >&2
echo " sudo dnf install cmake (Fedora)" >&2
fi
echo "" >&2
echo " Or run: anvil doctor --fix" >&2
exit 1
fi
if ! command -v g++ &>/dev/null && ! command -v clang++ &>/dev/null; then
die "No C++ compiler found (need g++ or clang++)."
echo " Or run: anvil doctor --fix" >&2
exit 1
fi
if ! command -v git &>/dev/null; then
die "git not found (needed to fetch Google Test)."
exit 1
fi
# -- Build -------------------------------------------------------------------
if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$BUILD_DIR" ]]; then
info "Cleaning build directory..."
rm -rf "$BUILD_DIR"
fi
if [[ ! -f "$BUILD_DIR/CMakeCache.txt" ]]; then
info "Configuring test build. First run will fetch Google Test..."
cmake -S "$TEST_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug 2>&1 | \
while IFS= read -r line; do echo " $line"; done
echo ""
fi
info "Building tests..."
cmake --build "$BUILD_DIR" --parallel 2>&1 | \
while IFS= read -r line; do echo " $line"; done
echo ""
info "${BLD}Running tests...${RST}"
echo ""
# -- Run ---------------------------------------------------------------------
CTEST_ARGS=("--test-dir" "$BUILD_DIR" "--output-on-failure")
[[ -n "$VERBOSE" ]] && CTEST_ARGS+=("--verbose")
[[ -n "$FILTER" ]] && CTEST_ARGS+=($FILTER)
if ctest "${CTEST_ARGS[@]}"; then
echo ""
ok "${BLD}All tests passed.${RST}"
else
echo ""
die "Some tests failed."
fi

View File

@@ -30,7 +30,19 @@ include_directories(
) )
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Unit tests (Google Mock) # Mock Arduino library -- provides Arduino API on x86_64
# --------------------------------------------------------------------------
add_library(mock_arduino STATIC
mocks/mock_arduino.cpp
)
target_include_directories(mock_arduino PUBLIC
${CMAKE_SOURCE_DIR}/mocks
${LIB_DIR}/hal
${LIB_DIR}/app
)
# --------------------------------------------------------------------------
# Unit tests (Google Mock -- verifies exact HAL call sequences)
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
add_executable(test_unit add_executable(test_unit
test_unit.cpp test_unit.cpp
@@ -38,6 +50,18 @@ add_executable(test_unit
target_link_libraries(test_unit target_link_libraries(test_unit
GTest::gtest_main GTest::gtest_main
GTest::gmock GTest::gmock
mock_arduino
)
# --------------------------------------------------------------------------
# System tests (SimHal -- exercises full application logic)
# --------------------------------------------------------------------------
add_executable(test_system
test_system.cpp
)
target_link_libraries(test_system
GTest::gtest_main
mock_arduino
) )
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@@ -45,3 +69,4 @@ target_link_libraries(test_unit
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
include(GoogleTest) include(GoogleTest)
gtest_discover_tests(test_unit) gtest_discover_tests(test_unit)
gtest_discover_tests(test_system)

View File

@@ -0,0 +1,15 @@
/*
* Arduino.h -- shim for host-side test builds.
*
* When compiling tests on x86_64, this file sits in the mocks/ include
* path and intercepts #include <Arduino.h> so that student code (or
* future libraries) that reference Arduino directly still compiles.
*
* Generated by Anvil.
*/
#ifndef ARDUINO_H_MOCK_SHIM
#define ARDUINO_H_MOCK_SHIM
#include "mock_arduino.h"
#endif // ARDUINO_H_MOCK_SHIM

View File

@@ -0,0 +1,55 @@
/*
* SPI.h -- shim for host-side test builds.
*
* Intercepts #include <SPI.h> so that code referencing SPI compiles
* on x86_64. Provides a minimal mock of the Arduino SPI library.
*
* Generated by Anvil.
*/
#ifndef SPI_H_MOCK_SHIM
#define SPI_H_MOCK_SHIM
#include "mock_arduino.h"
#define SPI_MODE0 0x00
#define SPI_MODE1 0x04
#define SPI_MODE2 0x08
#define SPI_MODE3 0x0C
#define SPI_CLOCK_DIV2 0x04
#define SPI_CLOCK_DIV4 0x00
#define SPI_CLOCK_DIV8 0x05
#define SPI_CLOCK_DIV16 0x01
#define SPI_CLOCK_DIV32 0x06
#define SPI_CLOCK_DIV64 0x02
#define SPI_CLOCK_DIV128 0x03
#define MSBFIRST 1
#define LSBFIRST 0
struct SPISettings {
uint32_t clock;
uint8_t bitOrder;
uint8_t dataMode;
SPISettings() : clock(4000000), bitOrder(MSBFIRST), dataMode(SPI_MODE0) {}
SPISettings(uint32_t c, uint8_t o, uint8_t m)
: clock(c), bitOrder(o), dataMode(m) {}
};
class MockSPI {
public:
void begin() {}
void end() {}
void beginTransaction(SPISettings) {}
void endTransaction() {}
uint8_t transfer(uint8_t data) { (void)data; return 0; }
uint16_t transfer16(uint16_t data) { (void)data; return 0; }
void transfer(void* buf, size_t count) { (void)buf; (void)count; }
void setBitOrder(uint8_t) {}
void setClockDivider(uint8_t) {}
void setDataMode(uint8_t) {}
};
extern MockSPI SPI;
#endif // SPI_H_MOCK_SHIM

View File

@@ -0,0 +1,35 @@
/*
* Wire.h -- shim for host-side test builds.
*
* Intercepts #include <Wire.h> so that code referencing Wire compiles
* on x86_64. The mock Arduino state handles I2C through the HAL layer,
* but if student code or libraries reference Wire directly, this
* prevents a compile error.
*
* Generated by Anvil.
*/
#ifndef WIRE_H_MOCK_SHIM
#define WIRE_H_MOCK_SHIM
#include "mock_arduino.h"
class MockWire {
public:
void begin() {}
void begin(uint8_t) {}
void beginTransmission(uint8_t) {}
uint8_t endTransmission(bool sendStop = true) { (void)sendStop; return 0; }
uint8_t requestFrom(uint8_t addr, uint8_t count, bool sendStop = true) {
(void)addr; (void)count; (void)sendStop;
return 0;
}
size_t write(uint8_t data) { (void)data; return 1; }
size_t write(const uint8_t* data, size_t len) { (void)data; return len; }
int available() { return 0; }
int read() { return -1; }
void setClock(uint32_t freq) { (void)freq; }
};
extern MockWire Wire;
#endif // WIRE_H_MOCK_SHIM

View File

@@ -0,0 +1,579 @@
/*
* mock_arduino.cpp -- Implementation of the Arduino API mock.
*
* This file provides function bodies for all Arduino core functions
* and the test control API. Linked into the test executable via cmake.
*
* Generated by Anvil -- https://github.com/nexus-workshops/anvil
*/
#include "mock_arduino.h"
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <cctype>
#include <sstream>
// ============================================================================
// Global state
// ============================================================================
MockArduinoState _mock_arduino;
MockSerial Serial;
// Wire mock (defined in Wire.h shim)
#include "Wire.h"
MockWire Wire;
// SPI mock (defined in SPI.h shim)
#include "SPI.h"
MockSPI SPI;
// ============================================================================
// Test control API
// ============================================================================
void mock_arduino_reset() {
memset(_mock_arduino.pin_modes, 0, sizeof(_mock_arduino.pin_modes));
memset(_mock_arduino.pin_digital, 0, sizeof(_mock_arduino.pin_digital));
memset(_mock_arduino.pin_analog, 0, sizeof(_mock_arduino.pin_analog));
memset(_mock_arduino.pin_pwm, 0, sizeof(_mock_arduino.pin_pwm));
_mock_arduino.millis_value = 0;
_mock_arduino.micros_value = 0;
_mock_arduino.serial_output.clear();
_mock_arduino.serial_input.clear();
_mock_arduino.serial_baud = 0;
_mock_arduino.serial_begun = false;
for (int i = 0; i < 8; ++i) {
_mock_arduino.interrupts[i].callback = nullptr;
_mock_arduino.interrupts[i].mode = 0;
_mock_arduino.interrupts[i].attached = false;
}
_mock_arduino.interrupts_enabled = true;
_mock_arduino.gpio_log.clear();
_mock_arduino.delay_log.clear();
}
void mock_arduino_advance_millis(unsigned long ms) {
_mock_arduino.millis_value += ms;
_mock_arduino.micros_value += ms * 1000;
}
void mock_arduino_set_millis(unsigned long ms) {
_mock_arduino.millis_value = ms;
_mock_arduino.micros_value = ms * 1000;
}
void mock_arduino_set_digital(uint8_t pin, int value) {
if (pin < MOCK_ARDUINO_MAX_PINS) {
_mock_arduino.pin_digital[pin] = value;
}
}
void mock_arduino_set_analog(uint8_t pin, int value) {
if (pin < MOCK_ARDUINO_MAX_PINS) {
_mock_arduino.pin_analog[pin] = value;
}
}
int mock_arduino_get_digital(uint8_t pin) {
if (pin < MOCK_ARDUINO_MAX_PINS) return _mock_arduino.pin_digital[pin];
return LOW;
}
int mock_arduino_get_pwm(uint8_t pin) {
if (pin < MOCK_ARDUINO_MAX_PINS) return _mock_arduino.pin_pwm[pin];
return 0;
}
uint8_t mock_arduino_get_pin_mode(uint8_t pin) {
if (pin < MOCK_ARDUINO_MAX_PINS) return _mock_arduino.pin_modes[pin];
return 0;
}
const std::string& mock_arduino_serial_output() {
return _mock_arduino.serial_output;
}
void mock_arduino_inject_serial(const std::string& data) {
for (char c : data) {
_mock_arduino.serial_input.push_back(static_cast<uint8_t>(c));
}
}
void mock_arduino_clear_serial() {
_mock_arduino.serial_output.clear();
_mock_arduino.serial_input.clear();
}
const std::vector<MockArduinoState::GpioEvent>& mock_arduino_gpio_log() {
return _mock_arduino.gpio_log;
}
void mock_arduino_clear_gpio_log() {
_mock_arduino.gpio_log.clear();
}
int mock_arduino_count_writes(uint8_t pin, int value) {
int count = 0;
for (const auto& e : _mock_arduino.gpio_log) {
if (e.pin == pin && e.value == value && !e.is_analog) ++count;
}
return count;
}
// ============================================================================
// Arduino digital I/O
// ============================================================================
void pinMode(uint8_t pin, uint8_t mode) {
if (pin < MOCK_ARDUINO_MAX_PINS) {
_mock_arduino.pin_modes[pin] = mode;
if (mode == INPUT_PULLUP) {
_mock_arduino.pin_digital[pin] = HIGH;
}
}
}
void digitalWrite(uint8_t pin, uint8_t val) {
if (pin < MOCK_ARDUINO_MAX_PINS) {
_mock_arduino.pin_digital[pin] = val;
_mock_arduino.gpio_log.push_back({
_mock_arduino.millis_value, pin, val, false
});
}
}
int digitalRead(uint8_t pin) {
if (pin < MOCK_ARDUINO_MAX_PINS) return _mock_arduino.pin_digital[pin];
return LOW;
}
// ============================================================================
// Arduino analog I/O
// ============================================================================
int analogRead(uint8_t pin) {
if (pin < MOCK_ARDUINO_MAX_PINS) return _mock_arduino.pin_analog[pin];
return 0;
}
void analogWrite(uint8_t pin, int val) {
if (pin < MOCK_ARDUINO_MAX_PINS) {
_mock_arduino.pin_pwm[pin] = val;
_mock_arduino.pin_digital[pin] = (val > 0) ? HIGH : LOW;
_mock_arduino.gpio_log.push_back({
_mock_arduino.millis_value, pin, val, true
});
}
}
// ============================================================================
// Arduino timing
// ============================================================================
unsigned long millis() {
return _mock_arduino.millis_value;
}
unsigned long micros() {
return _mock_arduino.micros_value;
}
void delay(unsigned long ms) {
_mock_arduino.delay_log.push_back(ms);
_mock_arduino.millis_value += ms;
_mock_arduino.micros_value += ms * 1000;
}
void delayMicroseconds(unsigned long us) {
_mock_arduino.micros_value += us;
}
// ============================================================================
// Arduino math helpers
// ============================================================================
long map(long value, long fromLow, long fromHigh, long toLow, long toHigh) {
if (fromHigh == fromLow) return toLow;
return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
}
long constrain(long value, long low, long high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
static unsigned long _random_seed = 1;
long random(long max) {
if (max <= 0) return 0;
_random_seed = _random_seed * 1103515245 + 12345;
return (long)((_random_seed >> 16) % (unsigned long)max);
}
long random(long min, long max) {
if (max <= min) return min;
return random(max - min) + min;
}
void randomSeed(unsigned long seed) {
_random_seed = seed;
}
// ============================================================================
// Interrupts
// ============================================================================
void attachInterrupt(uint8_t interrupt, void (*callback)(), int mode) {
if (interrupt < 8) {
_mock_arduino.interrupts[interrupt].callback = callback;
_mock_arduino.interrupts[interrupt].mode = mode;
_mock_arduino.interrupts[interrupt].attached = true;
}
}
void detachInterrupt(uint8_t interrupt) {
if (interrupt < 8) {
_mock_arduino.interrupts[interrupt].callback = nullptr;
_mock_arduino.interrupts[interrupt].attached = false;
}
}
void noInterrupts() {
_mock_arduino.interrupts_enabled = false;
}
void interrupts() {
_mock_arduino.interrupts_enabled = true;
}
uint8_t digitalPinToInterrupt(uint8_t pin) {
// Uno/Nano: pin 2 = INT0, pin 3 = INT1
switch (pin) {
case 2: return 0;
case 3: return 1;
default: return 255; // NOT_AN_INTERRUPT
}
}
// ============================================================================
// MockSerial implementation
// ============================================================================
void MockSerial::begin(unsigned long baud) {
_mock_arduino.serial_baud = baud;
_mock_arduino.serial_begun = true;
}
void MockSerial::end() {
_mock_arduino.serial_begun = false;
}
size_t MockSerial::print(const char* str) {
if (!str) return 0;
_mock_arduino.serial_output += str;
return strlen(str);
}
size_t MockSerial::print(char c) {
_mock_arduino.serial_output += c;
return 1;
}
static std::string long_to_string(long val, int base) {
if (base == 16) {
char buf[32];
snprintf(buf, sizeof(buf), "%lx", val);
return buf;
}
if (base == 8) {
char buf[32];
snprintf(buf, sizeof(buf), "%lo", val);
return buf;
}
if (base == 2) {
if (val == 0) return "0";
std::string result;
unsigned long v = (unsigned long)val;
while (v > 0) {
result = (char)('0' + (v & 1)) + result;
v >>= 1;
}
return result;
}
return std::to_string(val);
}
static std::string ulong_to_string(unsigned long val, int base) {
return long_to_string((long)val, base);
}
size_t MockSerial::print(int val, int base) {
std::string s = long_to_string(val, base);
_mock_arduino.serial_output += s;
return s.size();
}
size_t MockSerial::print(unsigned int val, int base) {
std::string s = ulong_to_string(val, base);
_mock_arduino.serial_output += s;
return s.size();
}
size_t MockSerial::print(long val, int base) {
std::string s = long_to_string(val, base);
_mock_arduino.serial_output += s;
return s.size();
}
size_t MockSerial::print(unsigned long val, int base) {
std::string s = ulong_to_string(val, base);
_mock_arduino.serial_output += s;
return s.size();
}
size_t MockSerial::print(double val, int decimals) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*f", decimals, val);
_mock_arduino.serial_output += buf;
return strlen(buf);
}
size_t MockSerial::print(const std::string& str) {
_mock_arduino.serial_output += str;
return str.size();
}
size_t MockSerial::println(const char* str) {
size_t n = print(str);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(char c) {
size_t n = print(c);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(int val, int base) {
size_t n = print(val, base);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(unsigned int val, int base) {
size_t n = print(val, base);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(long val, int base) {
size_t n = print(val, base);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(unsigned long val, int base) {
size_t n = print(val, base);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(double val, int decimals) {
size_t n = print(val, decimals);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::println(const std::string& str) {
size_t n = print(str);
_mock_arduino.serial_output += "\n";
return n + 1;
}
size_t MockSerial::write(uint8_t b) {
_mock_arduino.serial_output += static_cast<char>(b);
return 1;
}
size_t MockSerial::write(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; ++i) {
_mock_arduino.serial_output += static_cast<char>(buf[i]);
}
return len;
}
int MockSerial::available() {
return static_cast<int>(_mock_arduino.serial_input.size());
}
int MockSerial::read() {
if (_mock_arduino.serial_input.empty()) return -1;
int c = _mock_arduino.serial_input.front();
_mock_arduino.serial_input.erase(_mock_arduino.serial_input.begin());
return c;
}
int MockSerial::peek() {
if (_mock_arduino.serial_input.empty()) return -1;
return _mock_arduino.serial_input.front();
}
void MockSerial::flush() {
// No-op in mock (real Arduino waits for TX buffer to drain)
}
// ============================================================================
// String class implementation
// ============================================================================
String::String(int val, int base) {
data_ = long_to_string(val, base);
}
String::String(unsigned int val, int base) {
data_ = ulong_to_string(val, base);
}
String::String(long val, int base) {
data_ = long_to_string(val, base);
}
String::String(unsigned long val, int base) {
data_ = ulong_to_string(val, base);
}
String::String(double val, int decimals) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*f", decimals, val);
data_ = buf;
}
char String::charAt(unsigned int index) const {
if (index < data_.size()) return data_[index];
return 0;
}
void String::setCharAt(unsigned int index, char c) {
if (index < data_.size()) data_[index] = c;
}
bool String::equalsIgnoreCase(const String& other) const {
if (data_.size() != other.data_.size()) return false;
for (size_t i = 0; i < data_.size(); ++i) {
if (tolower((unsigned char)data_[i]) !=
tolower((unsigned char)other.data_[i])) return false;
}
return true;
}
int String::compareTo(const String& other) const {
return data_.compare(other.data_);
}
int String::indexOf(char ch, unsigned int fromIndex) const {
auto pos = data_.find(ch, fromIndex);
return (pos == std::string::npos) ? -1 : (int)pos;
}
int String::indexOf(const String& str, unsigned int fromIndex) const {
auto pos = data_.find(str.data_, fromIndex);
return (pos == std::string::npos) ? -1 : (int)pos;
}
int String::lastIndexOf(char ch) const {
auto pos = data_.rfind(ch);
return (pos == std::string::npos) ? -1 : (int)pos;
}
String String::substring(unsigned int from, unsigned int to) const {
if (from >= data_.size()) return String();
if (to > data_.size()) to = (unsigned int)data_.size();
return String(data_.substr(from, to - from));
}
void String::toLowerCase() {
for (char& c : data_) c = (char)tolower((unsigned char)c);
}
void String::toUpperCase() {
for (char& c : data_) c = (char)toupper((unsigned char)c);
}
void String::trim() {
size_t start = data_.find_first_not_of(" \t\r\n");
size_t end = data_.find_last_not_of(" \t\r\n");
if (start == std::string::npos) {
data_.clear();
} else {
data_ = data_.substr(start, end - start + 1);
}
}
void String::replace(const String& from, const String& to) {
size_t pos = 0;
while ((pos = data_.find(from.data_, pos)) != std::string::npos) {
data_.replace(pos, from.data_.size(), to.data_);
pos += to.data_.size();
}
}
void String::remove(unsigned int index, unsigned int count) {
if (index < data_.size()) {
data_.erase(index, count);
}
}
long String::toInt() const {
return atol(data_.c_str());
}
float String::toFloat() const {
return (float)atof(data_.c_str());
}
double String::toDouble() const {
return atof(data_.c_str());
}
String& String::concat(const String& other) {
data_ += other.data_;
return *this;
}
String& String::concat(const char* s) {
if (s) data_ += s;
return *this;
}
String& String::concat(char c) {
data_ += c;
return *this;
}
String& String::concat(int val) {
data_ += std::to_string(val);
return *this;
}
String& String::concat(unsigned long val) {
data_ += std::to_string(val);
return *this;
}
String String::operator+(const String& rhs) const {
return String(data_ + rhs.data_);
}
char String::operator[](unsigned int index) const {
if (index < data_.size()) return data_[index];
return 0;
}
char& String::operator[](unsigned int index) {
return data_[index];
}

View File

@@ -0,0 +1,353 @@
#ifndef MOCK_ARDUINO_H
#define MOCK_ARDUINO_H
/*
* mock_arduino.h -- Arduino API mock for host-side (x86_64) testing.
*
* Provides the core Arduino vocabulary (types, constants, functions,
* Serial) so that application code compiles and runs on a PC without
* any AVR toolchain. All state is captured in a global MockArduino
* instance that tests can inspect and manipulate.
*
* This file replaces <Arduino.h> in the test build. The CMakeLists.txt
* adds the mocks/ directory to the include path, so any code that does
* #include <Arduino.h> will pick up this file instead.
*
* Usage in tests:
*
* #include "mock_arduino.h"
*
* TEST(MyTest, ReadsAnalogSensor) {
* mock_arduino_reset();
* mock_arduino_set_analog(A0, 512);
* int val = analogRead(A0);
* EXPECT_EQ(val, 512);
* }
*
* Generated by Anvil -- https://github.com/nexus-workshops/anvil
*/
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <cmath>
#include <string>
#include <vector>
#include <functional>
// ============================================================================
// Arduino types
// ============================================================================
typedef uint8_t byte;
typedef uint16_t word;
typedef bool boolean;
// ============================================================================
// Pin mode and logic constants
// ============================================================================
#ifndef INPUT
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#endif
#ifndef LOW
#define LOW 0x0
#define HIGH 0x1
#endif
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
// Analog pins (map to digital pin numbers, Uno layout by default)
#ifndef A0
#define A0 14
#define A1 15
#define A2 16
#define A3 17
#define A4 18
#define A5 19
#define A6 20
#define A7 21
// Extended for Mega
#define A8 54
#define A9 55
#define A10 56
#define A11 57
#define A12 58
#define A13 59
#define A14 60
#define A15 61
#endif
// ============================================================================
// Math constants
// ============================================================================
#ifndef PI
#define PI 3.14159265358979323846
#define HALF_PI 1.57079632679489661923
#define TWO_PI 6.28318530717958647692
#define DEG_TO_RAD 0.017453292519943295
#define RAD_TO_DEG 57.29577951308232
#endif
// ============================================================================
// Interrupt constants
// ============================================================================
#ifndef CHANGE
#define CHANGE 1
#define FALLING 2
#define RISING 3
#endif
// ============================================================================
// MockArduino -- global state for all Arduino functions
// ============================================================================
static const int MOCK_ARDUINO_MAX_PINS = 70; // Enough for Mega
struct MockArduinoState {
// Pin state
uint8_t pin_modes[MOCK_ARDUINO_MAX_PINS];
int pin_digital[MOCK_ARDUINO_MAX_PINS]; // digital values
int pin_analog[MOCK_ARDUINO_MAX_PINS]; // analog read values (0-1023)
int pin_pwm[MOCK_ARDUINO_MAX_PINS]; // analogWrite values (0-255)
// Timing
unsigned long millis_value;
unsigned long micros_value;
// Serial capture
std::string serial_output;
std::vector<uint8_t> serial_input;
unsigned long serial_baud;
bool serial_begun;
// Interrupt tracking
struct InterruptHandler {
void (*callback)();
int mode;
bool attached;
};
InterruptHandler interrupts[8]; // INT0-INT7
bool interrupts_enabled;
// GPIO event log
struct GpioEvent {
unsigned long timestamp;
uint8_t pin;
int value;
bool is_analog; // true = analogWrite, false = digitalWrite
};
std::vector<GpioEvent> gpio_log;
// Delay log (to verify timing behavior)
std::vector<unsigned long> delay_log;
};
// Global state -- tests access this directly
extern MockArduinoState _mock_arduino;
// ============================================================================
// Test control API -- call from tests to set up / inspect state
// ============================================================================
// Reset all state to defaults
void mock_arduino_reset();
// Clock control
void mock_arduino_advance_millis(unsigned long ms);
void mock_arduino_set_millis(unsigned long ms);
// Input injection
void mock_arduino_set_digital(uint8_t pin, int value);
void mock_arduino_set_analog(uint8_t pin, int value);
// Output inspection
int mock_arduino_get_digital(uint8_t pin);
int mock_arduino_get_pwm(uint8_t pin);
uint8_t mock_arduino_get_pin_mode(uint8_t pin);
// Serial inspection
const std::string& mock_arduino_serial_output();
void mock_arduino_inject_serial(const std::string& data);
void mock_arduino_clear_serial();
// GPIO log
const std::vector<MockArduinoState::GpioEvent>& mock_arduino_gpio_log();
void mock_arduino_clear_gpio_log();
int mock_arduino_count_writes(uint8_t pin, int value);
// ============================================================================
// Arduino core functions -- called by application code
// ============================================================================
// Digital I/O
void pinMode(uint8_t pin, uint8_t mode);
void digitalWrite(uint8_t pin, uint8_t val);
int digitalRead(uint8_t pin);
// Analog I/O
int analogRead(uint8_t pin);
void analogWrite(uint8_t pin, int val);
// Timing
unsigned long millis();
unsigned long micros();
void delay(unsigned long ms);
void delayMicroseconds(unsigned long us);
// Math helpers
long map(long value, long fromLow, long fromHigh, long toLow, long toHigh);
long constrain(long value, long low, long high);
long random(long max);
long random(long min, long max);
void randomSeed(unsigned long seed);
// Interrupts
void attachInterrupt(uint8_t interrupt, void (*callback)(), int mode);
void detachInterrupt(uint8_t interrupt);
void noInterrupts();
void interrupts();
uint8_t digitalPinToInterrupt(uint8_t pin);
// Bit manipulation (Arduino built-ins)
#ifndef bitRead
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, b) ((b) ? bitSet(value, bit) : bitClear(value, bit))
#define bit(n) (1UL << (n))
#define lowByte(w) ((uint8_t)((w) & 0xFF))
#define highByte(w) ((uint8_t)((w) >> 8))
#endif
// ============================================================================
// MockSerial -- replaces Serial global
// ============================================================================
class MockSerial {
public:
void begin(unsigned long baud);
void end();
size_t print(const char* str);
size_t print(char c);
size_t print(int val, int base = 10);
size_t print(unsigned int val, int base = 10);
size_t print(long val, int base = 10);
size_t print(unsigned long val, int base = 10);
size_t print(double val, int decimals = 2);
size_t print(const std::string& str);
size_t println(const char* str = "");
size_t println(char c);
size_t println(int val, int base = 10);
size_t println(unsigned int val, int base = 10);
size_t println(long val, int base = 10);
size_t println(unsigned long val, int base = 10);
size_t println(double val, int decimals = 2);
size_t println(const std::string& str);
size_t write(uint8_t b);
size_t write(const uint8_t* buf, size_t len);
int available();
int read();
int peek();
void flush();
explicit operator bool() const { return _mock_arduino.serial_begun; }
};
// Global Serial instance (matches Arduino's global)
extern MockSerial Serial;
// ============================================================================
// Minimal String class (Arduino-compatible subset)
// ============================================================================
class String {
public:
String() : data_() {}
String(const char* s) : data_(s ? s : "") {}
String(const std::string& s) : data_(s) {}
String(char c) : data_(1, c) {}
String(int val, int base = 10);
String(unsigned int val, int base = 10);
String(long val, int base = 10);
String(unsigned long val, int base = 10);
String(double val, int decimals = 2);
// Access
unsigned int length() const { return (unsigned int)data_.size(); }
const char* c_str() const { return data_.c_str(); }
char charAt(unsigned int index) const;
void setCharAt(unsigned int index, char c);
// Comparison
bool equals(const String& other) const { return data_ == other.data_; }
bool equalsIgnoreCase(const String& other) const;
int compareTo(const String& other) const;
// Search
int indexOf(char ch, unsigned int fromIndex = 0) const;
int indexOf(const String& str, unsigned int fromIndex = 0) const;
int lastIndexOf(char ch) const;
// Modification
String substring(unsigned int from, unsigned int to = 0xFFFFFFFF) const;
void toLowerCase();
void toUpperCase();
void trim();
void replace(const String& from, const String& to);
void remove(unsigned int index, unsigned int count = 1);
// Conversion
long toInt() const;
float toFloat() const;
double toDouble() const;
// Concatenation
String& concat(const String& other);
String& concat(const char* s);
String& concat(char c);
String& concat(int val);
String& concat(unsigned long val);
// Operators
String& operator+=(const String& rhs) { return concat(rhs); }
String& operator+=(const char* rhs) { return concat(rhs); }
String& operator+=(char rhs) { return concat(rhs); }
String operator+(const String& rhs) const;
bool operator==(const String& rhs) const { return data_ == rhs.data_; }
bool operator==(const char* rhs) const { return data_ == (rhs ? rhs : ""); }
bool operator!=(const String& rhs) const { return data_ != rhs.data_; }
bool operator<(const String& rhs) const { return data_ < rhs.data_; }
char operator[](unsigned int index) const;
char& operator[](unsigned int index);
// Interop with std::string
operator std::string() const { return data_; }
private:
std::string data_;
};
// ============================================================================
// Arduino.h compatibility alias
// ============================================================================
// So that #include <Arduino.h> resolves to this file via include path,
// we provide this comment as a note. The CMakeLists.txt adds the mocks/
// directory to the include path, so #include <Arduino.h> becomes:
// #include "mock_arduino.h" (via the Arduino.h shim in this directory)
#endif // MOCK_ARDUINO_H

View File

@@ -1,11 +1,37 @@
@echo off @echo off
setlocal setlocal enabledelayedexpansion
set "SCRIPT_DIR=%~dp0" set "SCRIPT_DIR=%~dp0"
:: %~dp0 always ends with \ which breaks cmake quoting ("path\" escapes the quote) :: %~dp0 always ends with \ which breaks cmake quoting ("path\" escapes the quote)
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "BUILD_DIR=%SCRIPT_DIR%\build" set "BUILD_DIR=%SCRIPT_DIR%\build"
:: -- Compiler detection (with vswhere fallback for MSVC) --------------------
set "HAS_COMPILER=0"
where g++ >nul 2>&1 && set "HAS_COMPILER=1"
where clang++ >nul 2>&1 && set "HAS_COMPILER=1"
where cl >nul 2>&1 && set "HAS_COMPILER=1"
if "%HAS_COMPILER%"=="1" goto :compiler_ok
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" goto :compiler_ok
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -property installationPath`) do (
set "VS_PATH=%%i"
)
if not defined VS_PATH goto :compiler_ok
set "VCVARS=!VS_PATH!\VC\Auxiliary\Build\vcvarsall.bat"
if not exist "!VCVARS!" goto :compiler_ok
echo Setting up MSVC environment...
call "!VCVARS!" x64 >nul 2>&1
:compiler_ok
:: -- Build and test ---------------------------------------------------------
if "%1"=="--clean" ( if "%1"=="--clean" (
if exist "%BUILD_DIR%" ( if exist "%BUILD_DIR%" (
echo Cleaning build directory... echo Cleaning build directory...
@@ -18,7 +44,7 @@ if not exist "%BUILD_DIR%\CMakeCache.txt" (
cmake -S "%SCRIPT_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Debug cmake -S "%SCRIPT_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Debug
if errorlevel 1 ( if errorlevel 1 (
echo FAIL: cmake configure failed. echo FAIL: cmake configure failed.
echo cmake is required for host-side tests. echo cmake and a C++ compiler are required for host-side tests.
echo Run 'anvil doctor' to see install instructions. echo Run 'anvil doctor' to see install instructions.
exit /b 1 exit /b 1
) )

View File

@@ -0,0 +1,148 @@
#include <gtest/gtest.h>
#include "mock_arduino.h"
#include "hal.h"
#include "sim_hal.h"
#include "{{PROJECT_NAME}}_app.h"
// ============================================================================
// System Tests -- exercise full application logic against simulated hardware
//
// Unlike unit tests (which verify exact call sequences), system tests
// run the real application code against SimHal and inspect observable
// outputs: pin states, serial messages, timing behavior.
// ============================================================================
class BlinkAppSystemTest : public ::testing::Test {
protected:
void SetUp() override {
mock_arduino_reset();
sim_.setMillis(0);
}
SimHal sim_;
};
TEST_F(BlinkAppSystemTest, SetupConfiguresPins) {
BlinkApp app(&sim_);
app.begin();
EXPECT_EQ(sim_.getPinMode(13), OUTPUT);
EXPECT_EQ(sim_.getPinMode(2), INPUT_PULLUP);
}
TEST_F(BlinkAppSystemTest, LedTogglesAtCorrectInterval) {
BlinkApp app(&sim_);
app.begin();
// LED should be off initially
EXPECT_EQ(sim_.getPin(13), LOW);
// Advance past one slow interval (500ms)
sim_.advanceMillis(500);
app.update();
EXPECT_EQ(sim_.getPin(13), HIGH);
// Advance another interval
sim_.advanceMillis(500);
app.update();
EXPECT_EQ(sim_.getPin(13), LOW);
}
TEST_F(BlinkAppSystemTest, LedDoesNotToggleTooEarly) {
BlinkApp app(&sim_);
app.begin();
sim_.advanceMillis(499);
app.update();
EXPECT_EQ(sim_.getPin(13), LOW);
}
TEST_F(BlinkAppSystemTest, ButtonTogglesFastMode) {
BlinkApp app(&sim_, 13, 2);
sim_.setPin(2, HIGH); // Button not pressed (INPUT_PULLUP)
app.begin();
EXPECT_FALSE(app.fastMode());
// Press button
sim_.setPin(2, LOW);
app.update();
EXPECT_TRUE(app.fastMode());
EXPECT_EQ(app.interval(), BlinkApp::FAST_INTERVAL_MS);
// Release and press again -> back to slow
sim_.setPin(2, HIGH);
app.update();
sim_.setPin(2, LOW);
app.update();
EXPECT_FALSE(app.fastMode());
EXPECT_EQ(app.interval(), BlinkApp::SLOW_INTERVAL_MS);
}
TEST_F(BlinkAppSystemTest, FastModeBlinksFaster) {
BlinkApp app(&sim_, 13, 2);
sim_.setPin(2, HIGH);
app.begin();
// Switch to fast mode
sim_.setPin(2, LOW);
app.update();
sim_.setPin(2, HIGH);
app.update();
EXPECT_TRUE(app.fastMode());
// Clear log, then count toggles over 1 second
sim_.clearGpioLog();
for (int i = 0; i < 8; ++i) {
sim_.advanceMillis(125);
app.update();
}
// In 1 second at 125ms intervals, we expect 8 toggles
int high_count = sim_.countWrites(13, HIGH);
int low_count = sim_.countWrites(13, LOW);
EXPECT_EQ(high_count + low_count, 8);
}
TEST_F(BlinkAppSystemTest, SerialOutputOnStartup) {
BlinkApp app(&sim_);
app.begin();
std::string output = sim_.serialOutput();
EXPECT_NE(output.find("BlinkApp started"), std::string::npos);
}
TEST_F(BlinkAppSystemTest, SerialOutputOnModeChange) {
BlinkApp app(&sim_, 13, 2);
sim_.setPin(2, HIGH);
app.begin();
sim_.clearSerialOutput();
// Press button
sim_.setPin(2, LOW);
app.update();
std::string output = sim_.serialOutput();
EXPECT_NE(output.find("FAST"), std::string::npos);
}
// ============================================================================
// Timing accuracy test -- verifies blink over a longer duration
// ============================================================================
TEST_F(BlinkAppSystemTest, TenSecondBlinkCount) {
BlinkApp app(&sim_);
app.begin();
sim_.clearGpioLog();
// Run for 10 seconds at 10ms resolution
for (int i = 0; i < 1000; ++i) {
sim_.advanceMillis(10);
app.update();
}
// At 500ms intervals over 10s, expect 20 toggles
int toggles = sim_.countWrites(13, HIGH) + sim_.countWrites(13, LOW);
EXPECT_EQ(toggles, 20);
}

View File

@@ -147,6 +147,38 @@ fn test_template_creates_test_infrastructure() {
tmp.path().join("test/mocks/sim_hal.h").exists(), tmp.path().join("test/mocks/sim_hal.h").exists(),
"sim_hal.h should exist" "sim_hal.h should exist"
); );
assert!(
tmp.path().join("test/mocks/mock_arduino.h").exists(),
"mock_arduino.h should exist"
);
assert!(
tmp.path().join("test/mocks/mock_arduino.cpp").exists(),
"mock_arduino.cpp should exist"
);
assert!(
tmp.path().join("test/mocks/Arduino.h").exists(),
"Arduino.h shim should exist"
);
assert!(
tmp.path().join("test/mocks/Wire.h").exists(),
"Wire.h shim should exist"
);
assert!(
tmp.path().join("test/mocks/SPI.h").exists(),
"SPI.h shim should exist"
);
assert!(
tmp.path().join("test/test_system.cpp").exists(),
"test_system.cpp should exist"
);
assert!(
tmp.path().join("test.sh").exists(),
"test.sh root script should exist"
);
assert!(
tmp.path().join("test.bat").exists(),
"test.bat root script should exist"
);
assert!( assert!(
tmp.path().join("test/run_tests.sh").exists(), tmp.path().join("test/run_tests.sh").exists(),
"run_tests.sh should exist" "run_tests.sh should exist"
@@ -834,10 +866,18 @@ fn test_full_project_structure() {
"_monitor_filter.ps1", "_monitor_filter.ps1",
"test/CMakeLists.txt", "test/CMakeLists.txt",
"test/test_unit.cpp", "test/test_unit.cpp",
"test/test_system.cpp",
"test/run_tests.sh", "test/run_tests.sh",
"test/run_tests.bat", "test/run_tests.bat",
"test/mocks/mock_hal.h", "test/mocks/mock_hal.h",
"test/mocks/sim_hal.h", "test/mocks/sim_hal.h",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test.sh",
"test.bat",
]; ];
for f in &expected_files { for f in &expected_files {
@@ -1414,9 +1454,18 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
"build.sh", "build.bat", "build.sh", "build.bat",
"upload.sh", "upload.bat", "upload.sh", "upload.bat",
"monitor.sh", "monitor.bat", "monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1", "_detect_port.ps1",
"_monitor_filter.ps1", "_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat", "test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
]; ];
for f in &refreshable { for f in &refreshable {
@@ -1461,7 +1510,7 @@ fn test_refresh_detects_modified_script() {
#[test] #[test]
fn test_refresh_does_not_list_user_files() { fn test_refresh_does_not_list_user_files() {
// .anvil.toml, source files, and config must never be refreshable. // .anvil.toml, source files, and user test code must never be refreshable.
let never_refreshable = vec![ let never_refreshable = vec![
".anvil.toml", ".anvil.toml",
".anvil.local", ".anvil.local",
@@ -1469,19 +1518,26 @@ fn test_refresh_does_not_list_user_files() {
".editorconfig", ".editorconfig",
".clang-format", ".clang-format",
"README.md", "README.md",
"test/CMakeLists.txt",
"test/test_unit.cpp", "test/test_unit.cpp",
"test/mocks/mock_hal.h", "test/test_system.cpp",
"test/mocks/sim_hal.h",
]; ];
let refreshable = vec![ let refreshable = vec![
"build.sh", "build.bat", "build.sh", "build.bat",
"upload.sh", "upload.bat", "upload.sh", "upload.bat",
"monitor.sh", "monitor.bat", "monitor.sh", "monitor.bat",
"test.sh", "test.bat",
"_detect_port.ps1", "_detect_port.ps1",
"_monitor_filter.ps1", "_monitor_filter.ps1",
"test/run_tests.sh", "test/run_tests.bat", "test/run_tests.sh", "test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
]; ];
for uf in &never_refreshable { for uf in &never_refreshable {
@@ -3226,3 +3282,205 @@ fn test_mixed_pins_and_buses_roundtrip() {
assert!(pc.buses.contains_key("i2c")); assert!(pc.buses.contains_key("i2c"));
assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8); assert_eq!(*pc.buses["spi"].user_pins.get("cs").unwrap(), 10u8);
} }
// ==========================================================================
// Mock Arduino: template file content verification
// ==========================================================================
#[test]
fn test_mock_arduino_header_has_core_api() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "mock_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let header = fs::read_to_string(tmp.path().join("test/mocks/mock_arduino.h")).unwrap();
// Core Arduino constants
assert!(header.contains("#define INPUT"), "Should define INPUT");
assert!(header.contains("#define OUTPUT"), "Should define OUTPUT");
assert!(header.contains("#define HIGH"), "Should define HIGH");
assert!(header.contains("#define LOW"), "Should define LOW");
assert!(header.contains("#define LED_BUILTIN"), "Should define LED_BUILTIN");
assert!(header.contains("#define A0"), "Should define A0");
// Core Arduino functions
assert!(header.contains("void pinMode("), "Should declare pinMode");
assert!(header.contains("void digitalWrite("), "Should declare digitalWrite");
assert!(header.contains("int digitalRead("), "Should declare digitalRead");
assert!(header.contains("int analogRead("), "Should declare analogRead");
assert!(header.contains("void analogWrite("), "Should declare analogWrite");
assert!(header.contains("unsigned long millis()"), "Should declare millis");
assert!(header.contains("void delay("), "Should declare delay");
// Serial class
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
assert!(header.contains("extern MockSerial Serial"), "Should declare global Serial");
// Test control API
assert!(header.contains("mock_arduino_reset()"), "Should have reset");
assert!(header.contains("mock_arduino_advance_millis("), "Should have advance_millis");
assert!(header.contains("mock_arduino_set_digital("), "Should have set_digital");
assert!(header.contains("mock_arduino_set_analog("), "Should have set_analog");
// String class
assert!(header.contains("class String"), "Should declare String class");
}
#[test]
fn test_mock_arduino_shims_exist() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "shim_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
// Arduino.h shim should include mock_arduino.h
let arduino_h = fs::read_to_string(tmp.path().join("test/mocks/Arduino.h")).unwrap();
assert!(
arduino_h.contains("mock_arduino.h"),
"Arduino.h shim should redirect to mock_arduino.h"
);
// Wire.h shim should provide MockWire
let wire_h = fs::read_to_string(tmp.path().join("test/mocks/Wire.h")).unwrap();
assert!(wire_h.contains("class MockWire"), "Wire.h should declare MockWire");
assert!(wire_h.contains("extern MockWire Wire"), "Wire.h should declare global Wire");
// SPI.h shim should provide MockSPI
let spi_h = fs::read_to_string(tmp.path().join("test/mocks/SPI.h")).unwrap();
assert!(spi_h.contains("class MockSPI"), "SPI.h should declare MockSPI");
assert!(spi_h.contains("extern MockSPI SPI"), "SPI.h should declare global SPI");
assert!(spi_h.contains("SPI_MODE0"), "SPI.h should define SPI modes");
assert!(spi_h.contains("struct SPISettings"), "SPI.h should define SPISettings");
}
#[test]
fn test_mock_arduino_all_files_ascii() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "ascii_mock".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let mock_files = vec![
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
];
for filename in &mock_files {
let content = fs::read_to_string(tmp.path().join(filename)).unwrap();
for (line_num, line) in content.lines().enumerate() {
for (col, ch) in line.chars().enumerate() {
assert!(
ch.is_ascii(),
"Non-ASCII in {} at {}:{}: '{}' (U+{:04X})",
filename, line_num + 1, col + 1, ch, ch as u32
);
}
}
}
}
#[test]
fn test_cmake_links_mock_arduino() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "cmake_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let cmake = fs::read_to_string(tmp.path().join("test/CMakeLists.txt")).unwrap();
// Should define mock_arduino library
assert!(cmake.contains("add_library(mock_arduino"), "Should define mock_arduino library");
assert!(cmake.contains("mock_arduino.cpp"), "Should compile mock_arduino.cpp");
// Both test targets should link mock_arduino
assert!(cmake.contains("target_link_libraries(test_unit"), "Should have test_unit target");
assert!(cmake.contains("target_link_libraries(test_system"), "Should have test_system target");
// System test target
assert!(cmake.contains("add_executable(test_system"), "Should build test_system");
assert!(cmake.contains("test_system.cpp"), "Should compile test_system.cpp");
// gtest discovery for both
assert!(cmake.contains("gtest_discover_tests(test_unit)"), "Should discover unit tests");
assert!(cmake.contains("gtest_discover_tests(test_system)"), "Should discover system tests");
}
#[test]
fn test_system_test_template_uses_simhal() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "sys_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
let system_test = fs::read_to_string(tmp.path().join("test/test_system.cpp")).unwrap();
// Should include mock_arduino and sim_hal
assert!(system_test.contains("mock_arduino.h"), "Should include mock_arduino.h");
assert!(system_test.contains("sim_hal.h"), "Should include sim_hal.h");
assert!(system_test.contains("sys_test_app.h"), "Should reference project app header");
// Should use SimHal, not MockHal
assert!(system_test.contains("SimHal"), "Should use SimHal for system tests");
// Should call mock_arduino_reset
assert!(system_test.contains("mock_arduino_reset()"), "Should reset mock state in SetUp");
}
#[test]
fn test_root_test_scripts_exist_and_reference_test_dir() {
let tmp = TempDir::new().unwrap();
let ctx = TemplateContext {
project_name: "script_test".to_string(),
anvil_version: "1.0.0".to_string(),
board_name: "uno".to_string(),
fqbn: "arduino:avr:uno".to_string(),
baud: 115200,
};
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
// test.sh
let test_sh = fs::read_to_string(tmp.path().join("test.sh")).unwrap();
assert!(test_sh.contains("TEST_DIR="), "test.sh should set TEST_DIR");
assert!(test_sh.contains("cmake"), "test.sh should invoke cmake");
assert!(test_sh.contains("ctest"), "test.sh should invoke ctest");
assert!(test_sh.contains("--unit"), "test.sh should support --unit flag");
assert!(test_sh.contains("--system"), "test.sh should support --system flag");
assert!(test_sh.contains("--clean"), "test.sh should support --clean flag");
// test.bat
let test_bat = fs::read_to_string(tmp.path().join("test.bat")).unwrap();
assert!(test_bat.contains("TEST_DIR"), "test.bat should set TEST_DIR");
assert!(test_bat.contains("cmake"), "test.bat should invoke cmake");
assert!(test_bat.contains("ctest"), "test.bat should invoke ctest");
assert!(test_bat.contains("--unit"), "test.bat should support --unit flag");
assert!(test_bat.contains("--system"), "test.bat should support --system flag");
}