diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs index e2fd5b4..3f3047f 100644 --- a/src/commands/doctor.rs +++ b/src/commands/doctor.rs @@ -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 { diff --git a/src/commands/refresh.rs b/src/commands/refresh.rs index 4610233..b1b41e2 100644 --- a/src/commands/refresh.rs +++ b/src/commands/refresh.rs @@ -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<()> { diff --git a/templates/basic/test.bat b/templates/basic/test.bat new file mode 100644 index 0000000..254fa6b --- /dev/null +++ b/templates/basic/test.bat @@ -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 \ No newline at end of file diff --git a/templates/basic/test.sh b/templates/basic/test.sh new file mode 100644 index 0000000..72a8051 --- /dev/null +++ b/templates/basic/test.sh @@ -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 \ No newline at end of file diff --git a/templates/basic/test/CMakeLists.txt.tmpl b/templates/basic/test/CMakeLists.txt.tmpl index 67b6565..9069cd0 100644 --- a/templates/basic/test/CMakeLists.txt.tmpl +++ b/templates/basic/test/CMakeLists.txt.tmpl @@ -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) diff --git a/templates/basic/test/mocks/Arduino.h b/templates/basic/test/mocks/Arduino.h new file mode 100644 index 0000000..da02045 --- /dev/null +++ b/templates/basic/test/mocks/Arduino.h @@ -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 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 \ No newline at end of file diff --git a/templates/basic/test/mocks/SPI.h b/templates/basic/test/mocks/SPI.h new file mode 100644 index 0000000..63db9a2 --- /dev/null +++ b/templates/basic/test/mocks/SPI.h @@ -0,0 +1,55 @@ +/* + * SPI.h -- shim for host-side test builds. + * + * Intercepts #include 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 \ No newline at end of file diff --git a/templates/basic/test/mocks/Wire.h b/templates/basic/test/mocks/Wire.h new file mode 100644 index 0000000..610507d --- /dev/null +++ b/templates/basic/test/mocks/Wire.h @@ -0,0 +1,35 @@ +/* + * Wire.h -- shim for host-side test builds. + * + * Intercepts #include 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 \ No newline at end of file diff --git a/templates/basic/test/mocks/mock_arduino.cpp b/templates/basic/test/mocks/mock_arduino.cpp new file mode 100644 index 0000000..ff8b47f --- /dev/null +++ b/templates/basic/test/mocks/mock_arduino.cpp @@ -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 +#include +#include +#include +#include + +// ============================================================================ +// 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(c)); + } +} + +void mock_arduino_clear_serial() { + _mock_arduino.serial_output.clear(); + _mock_arduino.serial_input.clear(); +} + +const std::vector& 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(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(buf[i]); + } + return len; +} + +int MockSerial::available() { + return static_cast(_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]; +} \ No newline at end of file diff --git a/templates/basic/test/mocks/mock_arduino.h b/templates/basic/test/mocks/mock_arduino.h new file mode 100644 index 0000000..8a4a7ec --- /dev/null +++ b/templates/basic/test/mocks/mock_arduino.h @@ -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 in the test build. The CMakeLists.txt + * adds the mocks/ directory to the include path, so any code that does + * #include 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 +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// 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 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 gpio_log; + + // Delay log (to verify timing behavior) + std::vector 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& 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 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 becomes: +// #include "mock_arduino.h" (via the Arduino.h shim in this directory) + +#endif // MOCK_ARDUINO_H \ No newline at end of file diff --git a/templates/basic/test/run_tests.bat b/templates/basic/test/run_tests.bat index eba2de7..53d0ca9 100644 --- a/templates/basic/test/run_tests.bat +++ b/templates/basic/test/run_tests.bat @@ -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 ) diff --git a/templates/basic/test/test_system.cpp.tmpl b/templates/basic/test/test_system.cpp.tmpl new file mode 100644 index 0000000..1f17c68 --- /dev/null +++ b/templates/basic/test/test_system.cpp.tmpl @@ -0,0 +1,148 @@ +#include + +#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); +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index a6323bd..c326328 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -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"); } \ No newline at end of file