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:
@@ -13,6 +13,7 @@ pub struct SystemHealth {
|
||||
pub dialout_ok: bool,
|
||||
pub cmake_ok: bool,
|
||||
pub cpp_compiler_ok: bool,
|
||||
pub cpp_on_path: bool,
|
||||
pub git_ok: bool,
|
||||
pub ports_found: usize,
|
||||
}
|
||||
@@ -99,7 +100,14 @@ pub fn check_system_health() -> SystemHealth {
|
||||
let cmake_ok = which::which("cmake").is_ok();
|
||||
|
||||
// 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
|
||||
let git_ok = which::which("git").is_ok();
|
||||
@@ -115,6 +123,7 @@ pub fn check_system_health() -> SystemHealth {
|
||||
dialout_ok,
|
||||
cmake_ok,
|
||||
cpp_compiler_ok,
|
||||
cpp_on_path,
|
||||
git_ok,
|
||||
ports_found,
|
||||
}
|
||||
@@ -130,6 +139,20 @@ fn has_cpp_compiler() -> bool {
|
||||
if which::which("cl").is_ok() {
|
||||
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
|
||||
}
|
||||
@@ -217,8 +240,14 @@ fn print_diagnostics(health: &SystemHealth) {
|
||||
}
|
||||
|
||||
// C++ compiler
|
||||
if health.cpp_compiler_ok {
|
||||
if health.cpp_on_path {
|
||||
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 {
|
||||
println!(
|
||||
" {} C++ compiler {}",
|
||||
@@ -655,16 +684,16 @@ fn fix_spec_cmake(pm: Option<&str>) -> FixSpec {
|
||||
|
||||
fn fix_spec_cpp(pm: Option<&str>) -> FixSpec {
|
||||
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 {
|
||||
prompt: "Install MinGW g++ via Chocolatey?",
|
||||
prompt: "Install MinGW g++ via Chocolatey? (recommended -- lands on PATH)",
|
||||
program: "choco",
|
||||
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 {
|
||||
message: "run: xcode-select --install",
|
||||
},
|
||||
@@ -751,7 +780,7 @@ fn hint_cmake() -> &'static str {
|
||||
|
||||
fn hint_cpp_compiler() -> &'static str {
|
||||
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") {
|
||||
"install: xcode-select --install"
|
||||
} else {
|
||||
|
||||
@@ -16,10 +16,20 @@ const REFRESHABLE_FILES: &[&str] = &[
|
||||
"upload.bat",
|
||||
"monitor.sh",
|
||||
"monitor.bat",
|
||||
"test.sh",
|
||||
"test.bat",
|
||||
"_detect_port.ps1",
|
||||
"_monitor_filter.ps1",
|
||||
"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",
|
||||
];
|
||||
|
||||
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
|
||||
158
templates/basic/test.bat
Normal file
158
templates/basic/test.bat
Normal 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
124
templates/basic/test.sh
Normal 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
|
||||
@@ -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
|
||||
test_unit.cpp
|
||||
@@ -38,6 +50,18 @@ add_executable(test_unit
|
||||
target_link_libraries(test_unit
|
||||
GTest::gtest_main
|
||||
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)
|
||||
gtest_discover_tests(test_unit)
|
||||
gtest_discover_tests(test_system)
|
||||
|
||||
15
templates/basic/test/mocks/Arduino.h
Normal file
15
templates/basic/test/mocks/Arduino.h
Normal 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
|
||||
55
templates/basic/test/mocks/SPI.h
Normal file
55
templates/basic/test/mocks/SPI.h
Normal 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
|
||||
35
templates/basic/test/mocks/Wire.h
Normal file
35
templates/basic/test/mocks/Wire.h
Normal 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
|
||||
579
templates/basic/test/mocks/mock_arduino.cpp
Normal file
579
templates/basic/test/mocks/mock_arduino.cpp
Normal 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];
|
||||
}
|
||||
353
templates/basic/test/mocks/mock_arduino.h
Normal file
353
templates/basic/test/mocks/mock_arduino.h
Normal 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
|
||||
@@ -1,11 +1,37 @@
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
:: %~dp0 always ends with \ which breaks cmake quoting ("path\" escapes the quote)
|
||||
set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
|
||||
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 exist "%BUILD_DIR%" (
|
||||
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
|
||||
if errorlevel 1 (
|
||||
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.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
148
templates/basic/test/test_system.cpp.tmpl
Normal file
148
templates/basic/test/test_system.cpp.tmpl
Normal 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);
|
||||
}
|
||||
@@ -147,6 +147,38 @@ fn test_template_creates_test_infrastructure() {
|
||||
tmp.path().join("test/mocks/sim_hal.h").exists(),
|
||||
"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!(
|
||||
tmp.path().join("test/run_tests.sh").exists(),
|
||||
"run_tests.sh should exist"
|
||||
@@ -834,10 +866,18 @@ fn test_full_project_structure() {
|
||||
"_monitor_filter.ps1",
|
||||
"test/CMakeLists.txt",
|
||||
"test/test_unit.cpp",
|
||||
"test/test_system.cpp",
|
||||
"test/run_tests.sh",
|
||||
"test/run_tests.bat",
|
||||
"test/mocks/mock_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 {
|
||||
@@ -1414,9 +1454,18 @@ fn test_refresh_freshly_extracted_is_up_to_date() {
|
||||
"build.sh", "build.bat",
|
||||
"upload.sh", "upload.bat",
|
||||
"monitor.sh", "monitor.bat",
|
||||
"test.sh", "test.bat",
|
||||
"_detect_port.ps1",
|
||||
"_monitor_filter.ps1",
|
||||
"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 {
|
||||
@@ -1461,7 +1510,7 @@ fn test_refresh_detects_modified_script() {
|
||||
|
||||
#[test]
|
||||
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![
|
||||
".anvil.toml",
|
||||
".anvil.local",
|
||||
@@ -1469,19 +1518,26 @@ fn test_refresh_does_not_list_user_files() {
|
||||
".editorconfig",
|
||||
".clang-format",
|
||||
"README.md",
|
||||
"test/CMakeLists.txt",
|
||||
"test/test_unit.cpp",
|
||||
"test/mocks/mock_hal.h",
|
||||
"test/mocks/sim_hal.h",
|
||||
"test/test_system.cpp",
|
||||
];
|
||||
|
||||
let refreshable = vec![
|
||||
"build.sh", "build.bat",
|
||||
"upload.sh", "upload.bat",
|
||||
"monitor.sh", "monitor.bat",
|
||||
"test.sh", "test.bat",
|
||||
"_detect_port.ps1",
|
||||
"_monitor_filter.ps1",
|
||||
"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 {
|
||||
@@ -3225,4 +3281,206 @@ fn test_mixed_pins_and_buses_roundtrip() {
|
||||
assert!(pc.buses.contains_key("spi"));
|
||||
assert!(pc.buses.contains_key("i2c"));
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user