Arduino CLI build system with HAL-based test architecture
Build and upload tool (arduino-build.sh): - Compile, upload, and monitor via arduino-cli - Device discovery with USB ID identification (--devices) - Persistent reconnecting serial monitor (--watch) - Split compile/upload workflow (--verify, --upload-only) - First-time setup wizard (--setup) - Comprehensive --help with troubleshooting and RedBoard specs Testable application architecture: - Hardware abstraction layer (lib/hal/) decouples logic from Arduino API - Google Mock HAL for unit tests (exact call verification) - Simulated HAL for system tests (GPIO state, virtual clock, I2C devices) - Example I2C temperature sensor simulator (TMP102) - Host-side test suite via CMake + Google Test (21 tests) Example sketch: - blink/ -- LED blink with button-controlled speed, wired through HAL
This commit is contained in:
70
test/CMakeLists.txt
Normal file
70
test/CMakeLists.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(sparkfun_tests LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Google Test (fetched automatically)
|
||||
# --------------------------------------------------------------------------
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0
|
||||
)
|
||||
# Prevent gtest from overriding compiler/linker options on Windows
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
enable_testing()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Include paths -- same headers used by Arduino, but compiled on host
|
||||
# --------------------------------------------------------------------------
|
||||
set(LIB_DIR ${CMAKE_SOURCE_DIR}/../lib)
|
||||
|
||||
include_directories(
|
||||
${LIB_DIR}/hal # hal.h
|
||||
${LIB_DIR}/app # blink_app.h
|
||||
${CMAKE_SOURCE_DIR}/mocks # mock_hal.h, sim_hal.h
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Unit tests (Google Mock)
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_blink_unit
|
||||
test_blink_unit.cpp
|
||||
)
|
||||
target_link_libraries(test_blink_unit
|
||||
GTest::gtest_main
|
||||
GTest::gmock
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# System tests (SimHal)
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_blink_system
|
||||
test_blink_system.cpp
|
||||
)
|
||||
target_link_libraries(test_blink_system
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# I2C example tests
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_i2c_example
|
||||
test_i2c_example.cpp
|
||||
)
|
||||
target_link_libraries(test_i2c_example
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Register with CTest
|
||||
# --------------------------------------------------------------------------
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(test_blink_unit)
|
||||
gtest_discover_tests(test_blink_system)
|
||||
gtest_discover_tests(test_i2c_example)
|
||||
45
test/mocks/mock_hal.h
Normal file
45
test/mocks/mock_hal.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef MOCK_HAL_H
|
||||
#define MOCK_HAL_H
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include "hal.h"
|
||||
|
||||
/*
|
||||
* StrictMock-friendly HAL mock for unit tests.
|
||||
*
|
||||
* Use this when you want to verify exact call sequences:
|
||||
* EXPECT_CALL(mock, digitalWrite(13, HIGH)).Times(1);
|
||||
*/
|
||||
class MockHal : public Hal {
|
||||
public:
|
||||
// GPIO
|
||||
MOCK_METHOD(void, pinMode, (uint8_t pin, uint8_t mode), (override));
|
||||
MOCK_METHOD(void, digitalWrite, (uint8_t pin, uint8_t value), (override));
|
||||
MOCK_METHOD(uint8_t, digitalRead, (uint8_t pin), (override));
|
||||
MOCK_METHOD(int, analogRead, (uint8_t pin), (override));
|
||||
MOCK_METHOD(void, analogWrite, (uint8_t pin, int value), (override));
|
||||
|
||||
// Timing
|
||||
MOCK_METHOD(unsigned long, millis, (), (override));
|
||||
MOCK_METHOD(unsigned long, micros, (), (override));
|
||||
MOCK_METHOD(void, delay, (unsigned long ms), (override));
|
||||
MOCK_METHOD(void, delayMicroseconds, (unsigned long us), (override));
|
||||
|
||||
// Serial
|
||||
MOCK_METHOD(void, serialBegin, (unsigned long baud), (override));
|
||||
MOCK_METHOD(void, serialPrint, (const char* msg), (override));
|
||||
MOCK_METHOD(void, serialPrintln, (const char* msg), (override));
|
||||
MOCK_METHOD(int, serialAvailable, (), (override));
|
||||
MOCK_METHOD(int, serialRead, (), (override));
|
||||
|
||||
// I2C
|
||||
MOCK_METHOD(void, i2cBegin, (), (override));
|
||||
MOCK_METHOD(void, i2cBeginTransmission, (uint8_t addr), (override));
|
||||
MOCK_METHOD(size_t, i2cWrite, (uint8_t data), (override));
|
||||
MOCK_METHOD(uint8_t, i2cEndTransmission, (), (override));
|
||||
MOCK_METHOD(uint8_t, i2cRequestFrom, (uint8_t addr, uint8_t count), (override));
|
||||
MOCK_METHOD(int, i2cAvailable, (), (override));
|
||||
MOCK_METHOD(int, i2cRead, (), (override));
|
||||
};
|
||||
|
||||
#endif // MOCK_HAL_H
|
||||
256
test/mocks/sim_hal.h
Normal file
256
test/mocks/sim_hal.h
Normal file
@@ -0,0 +1,256 @@
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include "hal.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/*
|
||||
* Simulated HAL for system tests.
|
||||
*
|
||||
* Unlike MockHal (which verifies call expectations), SimHal actually
|
||||
* maintains state: pin values, a virtual clock, serial output capture,
|
||||
* and pluggable I2C device simulators.
|
||||
*
|
||||
* This lets you write system tests that exercise full application logic
|
||||
* against simulated hardware:
|
||||
*
|
||||
* SimHal sim;
|
||||
* BlinkApp app(&sim);
|
||||
* app.begin();
|
||||
*
|
||||
* sim.setPin(2, LOW); // simulate button press
|
||||
* sim.advanceMillis(600); // advance clock
|
||||
* app.update();
|
||||
*
|
||||
* EXPECT_EQ(sim.getPin(13), HIGH); // check LED state
|
||||
*/
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// I2C device simulator interface
|
||||
// --------------------------------------------------------------------
|
||||
class I2cDeviceSim {
|
||||
public:
|
||||
virtual ~I2cDeviceSim() = default;
|
||||
|
||||
// Called when master writes bytes to this device
|
||||
virtual void onReceive(const uint8_t* data, size_t len) = 0;
|
||||
|
||||
// Called when master requests bytes; fill response buffer
|
||||
virtual size_t onRequest(uint8_t* buf, size_t max_len) = 0;
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Simulated HAL
|
||||
// --------------------------------------------------------------------
|
||||
class SimHal : public Hal {
|
||||
public:
|
||||
static const int NUM_PINS = 20; // D0-D13 + A0-A5
|
||||
|
||||
SimHal() : clock_ms_(0), clock_us_(0) {
|
||||
memset(pin_modes_, 0, sizeof(pin_modes_));
|
||||
memset(pin_values_, 0, sizeof(pin_values_));
|
||||
}
|
||||
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
void pinMode(uint8_t pin, uint8_t mode) override {
|
||||
if (pin < NUM_PINS) {
|
||||
pin_modes_[pin] = mode;
|
||||
// INPUT_PULLUP defaults to HIGH
|
||||
if (mode == INPUT_PULLUP) {
|
||||
pin_values_[pin] = HIGH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void digitalWrite(uint8_t pin, uint8_t value) override {
|
||||
if (pin < NUM_PINS) {
|
||||
pin_values_[pin] = value;
|
||||
gpio_log_.push_back({clock_ms_, pin, value});
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t digitalRead(uint8_t pin) override {
|
||||
if (pin < NUM_PINS) return pin_values_[pin];
|
||||
return LOW;
|
||||
}
|
||||
|
||||
int analogRead(uint8_t pin) override {
|
||||
if (pin < NUM_PINS) return analog_values_[pin];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void analogWrite(uint8_t pin, int value) override {
|
||||
if (pin < NUM_PINS) pin_values_[pin] = (value > 0) ? HIGH : LOW;
|
||||
}
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
unsigned long millis() override { return clock_ms_; }
|
||||
unsigned long micros() override { return clock_us_; }
|
||||
void delay(unsigned long ms) override { advanceMillis(ms); }
|
||||
void delayMicroseconds(unsigned long us) override { clock_us_ += us; }
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
void serialBegin(unsigned long baud) override { (void)baud; }
|
||||
void serialPrint(const char* msg) override {
|
||||
serial_output_ += msg;
|
||||
}
|
||||
void serialPrintln(const char* msg) override {
|
||||
serial_output_ += msg;
|
||||
serial_output_ += "\n";
|
||||
}
|
||||
int serialAvailable() override {
|
||||
return static_cast<int>(serial_input_.size());
|
||||
}
|
||||
int serialRead() override {
|
||||
if (serial_input_.empty()) return -1;
|
||||
int c = serial_input_.front();
|
||||
serial_input_.erase(serial_input_.begin());
|
||||
return c;
|
||||
}
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
void i2cBegin() override {}
|
||||
|
||||
void i2cBeginTransmission(uint8_t addr) override {
|
||||
i2c_addr_ = addr;
|
||||
i2c_tx_buf_.clear();
|
||||
}
|
||||
|
||||
size_t i2cWrite(uint8_t data) override {
|
||||
i2c_tx_buf_.push_back(data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t i2cEndTransmission() override {
|
||||
auto it = i2c_devices_.find(i2c_addr_);
|
||||
if (it == i2c_devices_.end()) return 2; // NACK on address
|
||||
it->second->onReceive(i2c_tx_buf_.data(), i2c_tx_buf_.size());
|
||||
return 0; // success
|
||||
}
|
||||
|
||||
uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) override {
|
||||
i2c_rx_buf_.clear();
|
||||
auto it = i2c_devices_.find(addr);
|
||||
if (it == i2c_devices_.end()) return 0;
|
||||
uint8_t tmp[256];
|
||||
size_t n = it->second->onRequest(tmp, count);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
i2c_rx_buf_.push_back(tmp[i]);
|
||||
}
|
||||
return static_cast<uint8_t>(n);
|
||||
}
|
||||
|
||||
int i2cAvailable() override {
|
||||
return static_cast<int>(i2c_rx_buf_.size());
|
||||
}
|
||||
|
||||
int i2cRead() override {
|
||||
if (i2c_rx_buf_.empty()) return -1;
|
||||
int val = i2c_rx_buf_.front();
|
||||
i2c_rx_buf_.erase(i2c_rx_buf_.begin());
|
||||
return val;
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test control API (not part of Hal interface)
|
||||
// ====================================================================
|
||||
|
||||
// -- Clock control ------------------------------------------------------
|
||||
void advanceMillis(unsigned long ms) {
|
||||
clock_ms_ += ms;
|
||||
clock_us_ += ms * 1000;
|
||||
}
|
||||
|
||||
void setMillis(unsigned long ms) {
|
||||
clock_ms_ = ms;
|
||||
clock_us_ = ms * 1000;
|
||||
}
|
||||
|
||||
// -- GPIO control -------------------------------------------------------
|
||||
void setPin(uint8_t pin, uint8_t value) {
|
||||
if (pin < NUM_PINS) pin_values_[pin] = value;
|
||||
}
|
||||
|
||||
uint8_t getPin(uint8_t pin) const {
|
||||
if (pin < NUM_PINS) return pin_values_[pin];
|
||||
return LOW;
|
||||
}
|
||||
|
||||
uint8_t getPinMode(uint8_t pin) const {
|
||||
if (pin < NUM_PINS) return pin_modes_[pin];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setAnalog(uint8_t pin, int value) {
|
||||
analog_values_[pin] = value;
|
||||
}
|
||||
|
||||
// -- GPIO history -------------------------------------------------------
|
||||
struct GpioEvent {
|
||||
unsigned long timestamp_ms;
|
||||
uint8_t pin;
|
||||
uint8_t value;
|
||||
};
|
||||
|
||||
const std::vector<GpioEvent>& gpioLog() const { return gpio_log_; }
|
||||
|
||||
void clearGpioLog() { gpio_log_.clear(); }
|
||||
|
||||
// Count how many times a pin was set to a specific value
|
||||
int countWrites(uint8_t pin, uint8_t value) const {
|
||||
int count = 0;
|
||||
for (const auto& e : gpio_log_) {
|
||||
if (e.pin == pin && e.value == value) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// -- Serial control -----------------------------------------------------
|
||||
const std::string& serialOutput() const { return serial_output_; }
|
||||
void clearSerialOutput() { serial_output_.clear(); }
|
||||
|
||||
void injectSerialInput(const std::string& data) {
|
||||
for (char c : data) {
|
||||
serial_input_.push_back(static_cast<uint8_t>(c));
|
||||
}
|
||||
}
|
||||
|
||||
// -- I2C device registration --------------------------------------------
|
||||
void attachI2cDevice(uint8_t addr, I2cDeviceSim* device) {
|
||||
i2c_devices_[addr] = device;
|
||||
}
|
||||
|
||||
void detachI2cDevice(uint8_t addr) {
|
||||
i2c_devices_.erase(addr);
|
||||
}
|
||||
|
||||
private:
|
||||
// GPIO
|
||||
uint8_t pin_modes_[NUM_PINS];
|
||||
uint8_t pin_values_[NUM_PINS];
|
||||
std::map<uint8_t, int> analog_values_;
|
||||
std::vector<GpioEvent> gpio_log_;
|
||||
|
||||
// Timing
|
||||
unsigned long clock_ms_;
|
||||
unsigned long clock_us_;
|
||||
|
||||
// Serial
|
||||
std::string serial_output_;
|
||||
std::vector<uint8_t> serial_input_;
|
||||
|
||||
// I2C
|
||||
uint8_t i2c_addr_ = 0;
|
||||
std::vector<uint8_t> i2c_tx_buf_;
|
||||
std::vector<uint8_t> i2c_rx_buf_;
|
||||
std::map<uint8_t, I2cDeviceSim*> i2c_devices_;
|
||||
};
|
||||
|
||||
#endif // SIM_HAL_H
|
||||
84
test/run_tests.sh
Executable file
84
test/run_tests.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run_tests.sh -- Build and run host-side unit and system tests
|
||||
#
|
||||
# Usage:
|
||||
# ./test/run_tests.sh Build and run all tests
|
||||
# ./test/run_tests.sh --clean Clean rebuild
|
||||
# ./test/run_tests.sh --verbose Verbose test output
|
||||
# ./test/run_tests.sh --filter X Run only tests matching X
|
||||
#
|
||||
# Prerequisites:
|
||||
# cmake >= 3.14, g++ or clang++, git (for fetching gtest)
|
||||
#
|
||||
# First run will download Google Test (~30 seconds).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
|
||||
# Color output
|
||||
if [[ -t 1 ]]; then
|
||||
RED=$'\033[0;31m'; GRN=$'\033[0;32m'; CYN=$'\033[0;36m'
|
||||
BLD=$'\033[1m'; RST=$'\033[0m'
|
||||
else
|
||||
RED=''; GRN=''; CYN=''; BLD=''; RST=''
|
||||
fi
|
||||
|
||||
info() { echo -e "${CYN}[TEST]${RST} $*"; }
|
||||
ok() { echo -e "${GRN}[PASS]${RST} $*"; }
|
||||
die() { echo -e "${RED}[FAIL]${RST} $*" >&2; exit 1; }
|
||||
|
||||
# Parse args
|
||||
DO_CLEAN=0
|
||||
VERBOSE=""
|
||||
FILTER=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--clean) DO_CLEAN=1 ;;
|
||||
--verbose) VERBOSE="--verbose" ;;
|
||||
--filter) : ;; # next arg is the pattern
|
||||
-*) die "Unknown option: $arg" ;;
|
||||
*) FILTER="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check prerequisites
|
||||
command -v cmake &>/dev/null || die "cmake not found. Install: sudo apt install cmake"
|
||||
command -v g++ &>/dev/null || command -v clang++ &>/dev/null || die "No C++ compiler found"
|
||||
command -v git &>/dev/null || die "git not found (needed to fetch Google Test)"
|
||||
|
||||
# Clean if requested
|
||||
if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$BUILD_DIR" ]]; then
|
||||
info "Cleaning build directory..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
fi
|
||||
|
||||
# Configure
|
||||
if [[ ! -f "$BUILD_DIR/CMakeCache.txt" ]]; then
|
||||
info "Configuring (first run will fetch Google Test)..."
|
||||
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
|
||||
fi
|
||||
|
||||
# Build
|
||||
info "Building tests..."
|
||||
cmake --build "$BUILD_DIR" --parallel
|
||||
|
||||
# Run
|
||||
echo ""
|
||||
info "${BLD}Running tests...${RST}"
|
||||
echo ""
|
||||
|
||||
CTEST_ARGS=("--test-dir" "$BUILD_DIR" "--output-on-failure")
|
||||
[[ -n "$VERBOSE" ]] && CTEST_ARGS+=("--verbose")
|
||||
[[ -n "$FILTER" ]] && CTEST_ARGS+=("-R" "$FILTER")
|
||||
|
||||
if ctest "${CTEST_ARGS[@]}"; then
|
||||
echo ""
|
||||
ok "${BLD}All tests passed.${RST}"
|
||||
else
|
||||
echo ""
|
||||
die "Some tests failed."
|
||||
fi
|
||||
201
test/test_blink_system.cpp
Normal file
201
test/test_blink_system.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "sim_hal.h"
|
||||
#include "blink_app.h"
|
||||
|
||||
// ============================================================================
|
||||
// System Tests -- exercise full app behavior against simulated hardware
|
||||
// ============================================================================
|
||||
|
||||
class BlinkAppSystemTest : public ::testing::Test {
|
||||
protected:
|
||||
SimHal sim_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Basic blink behavior
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedBlinksAtSlowRate) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
// Run for 2 seconds, calling update every 10ms (like a real loop)
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// At 500ms intervals over 2000ms, we expect ~4 toggles
|
||||
// (0->500: toggle, 500->1000: toggle, 1000->1500: toggle, 1500->2000: toggle)
|
||||
int highs = sim_.countWrites(LED_BUILTIN, HIGH);
|
||||
int lows = sim_.countWrites(LED_BUILTIN, LOW);
|
||||
EXPECT_GE(highs, 2);
|
||||
EXPECT_GE(lows, 2);
|
||||
EXPECT_LE(highs + lows, 6); // should not over-toggle
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedBlinksAtFastRateAfterButtonPress) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Button starts HIGH (INPUT_PULLUP)
|
||||
EXPECT_EQ(sim_.getPin(2), HIGH);
|
||||
|
||||
// Run 1 second in slow mode
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
int slow_toggles = sim_.gpioLog().size();
|
||||
|
||||
// Press button
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release button
|
||||
sim_.setPin(2, HIGH);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
// Clear log and run another second in fast mode
|
||||
sim_.clearGpioLog();
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
int fast_toggles = sim_.gpioLog().size();
|
||||
|
||||
// Fast mode (125ms) should produce roughly 4x more toggles than slow (500ms)
|
||||
EXPECT_GT(fast_toggles, slow_toggles * 2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Button edge detection
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, ButtonDebounceOnlyTriggersOnFallingEdge) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Rapid button noise: HIGH-LOW-HIGH-LOW-HIGH
|
||||
uint8_t sequence[] = {HIGH, LOW, HIGH, LOW, HIGH};
|
||||
int mode_changes = 0;
|
||||
bool was_fast = false;
|
||||
|
||||
for (uint8_t val : sequence) {
|
||||
sim_.setPin(2, val);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
if (app.fastMode() != was_fast) {
|
||||
mode_changes++;
|
||||
was_fast = app.fastMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Each HIGH->LOW transition is a falling edge, so 2 edges
|
||||
// (positions 0->1 and 2->3), toggling fast->slow->fast or slow->fast->slow
|
||||
EXPECT_EQ(mode_changes, 2);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, ButtonHeldDoesNotRepeatToggle) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
// Press and hold for 50 update cycles
|
||||
sim_.setPin(2, LOW);
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Should have toggled exactly once (to fast mode)
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release and hold released for 50 cycles
|
||||
sim_.setPin(2, HIGH);
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
}
|
||||
|
||||
// Still fast -- release alone should not toggle
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Serial output verification
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PrintsStartupMessage) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("BlinkApp started"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PrintsModeChangeMessages) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
sim_.clearSerialOutput();
|
||||
|
||||
// Press button
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("FAST"), std::string::npos);
|
||||
|
||||
sim_.setPin(2, HIGH);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
sim_.clearSerialOutput();
|
||||
|
||||
// Press again
|
||||
sim_.setPin(2, LOW);
|
||||
sim_.advanceMillis(10);
|
||||
app.update();
|
||||
|
||||
EXPECT_NE(sim_.serialOutput().find("SLOW"), std::string::npos);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GPIO log / timing verification
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
TEST_F(BlinkAppSystemTest, LedToggleTimingIsCorrect) {
|
||||
BlinkApp app(&sim_);
|
||||
app.begin();
|
||||
|
||||
// Run for 3 seconds at 1ms resolution
|
||||
for (int i = 0; i < 3000; ++i) {
|
||||
sim_.advanceMillis(1);
|
||||
app.update();
|
||||
}
|
||||
|
||||
const auto& log = sim_.gpioLog();
|
||||
ASSERT_GE(log.size(), 4u);
|
||||
|
||||
// Check intervals between consecutive toggles on pin 13
|
||||
for (size_t i = 1; i < log.size(); ++i) {
|
||||
if (log[i].pin == LED_BUILTIN && log[i - 1].pin == LED_BUILTIN) {
|
||||
unsigned long delta = log[i].timestamp_ms - log[i - 1].timestamp_ms;
|
||||
// Should be approximately 500ms (+/- 10ms for loop granularity)
|
||||
EXPECT_NEAR(delta, 500, 10)
|
||||
<< "Toggle " << i << " at t=" << log[i].timestamp_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppSystemTest, PinModesAreConfiguredCorrectly) {
|
||||
BlinkApp app(&sim_, LED_BUILTIN, 2);
|
||||
app.begin();
|
||||
|
||||
EXPECT_EQ(sim_.getPinMode(LED_BUILTIN), OUTPUT);
|
||||
EXPECT_EQ(sim_.getPinMode(2), INPUT_PULLUP);
|
||||
}
|
||||
132
test/test_blink_unit.cpp
Normal file
132
test/test_blink_unit.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "mock_hal.h"
|
||||
#include "blink_app.h"
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AnyNumber;
|
||||
using ::testing::Return;
|
||||
using ::testing::InSequence;
|
||||
using ::testing::HasSubstr;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests -- verify exact HAL interactions
|
||||
// ============================================================================
|
||||
|
||||
class BlinkAppUnitTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Allow timing and serial calls by default so tests can focus
|
||||
// on the specific interactions they care about.
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(_)).WillByDefault(Return(HIGH));
|
||||
EXPECT_CALL(mock_, serialBegin(_)).Times(AnyNumber());
|
||||
EXPECT_CALL(mock_, serialPrintln(_)).Times(AnyNumber());
|
||||
EXPECT_CALL(mock_, millis()).Times(AnyNumber());
|
||||
}
|
||||
|
||||
::testing::NiceMock<MockHal> mock_;
|
||||
};
|
||||
|
||||
TEST_F(BlinkAppUnitTest, BeginConfiguresPins) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
EXPECT_CALL(mock_, pinMode(13, OUTPUT)).Times(1);
|
||||
EXPECT_CALL(mock_, pinMode(2, INPUT_PULLUP)).Times(1);
|
||||
EXPECT_CALL(mock_, serialBegin(115200)).Times(1);
|
||||
|
||||
app.begin();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, StartsInSlowMode) {
|
||||
BlinkApp app(&mock_);
|
||||
app.begin();
|
||||
|
||||
EXPECT_FALSE(app.fastMode());
|
||||
EXPECT_EQ(app.interval(), BlinkApp::SLOW_INTERVAL_MS);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, TogglesLedAfterInterval) {
|
||||
BlinkApp app(&mock_);
|
||||
|
||||
// begin() at t=0
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
app.begin();
|
||||
|
||||
// update() at t=500 should toggle LED
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(500));
|
||||
EXPECT_CALL(mock_, digitalWrite(13, _)).Times(1);
|
||||
app.update();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, DoesNotToggleBeforeInterval) {
|
||||
BlinkApp app(&mock_);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
app.begin();
|
||||
|
||||
// update() at t=499 -- not enough time, no toggle
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(499));
|
||||
EXPECT_CALL(mock_, digitalWrite(_, _)).Times(0);
|
||||
app.update();
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, ButtonPressSwitchesToFastMode) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// Simulate button press (falling edge: HIGH -> LOW)
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
EXPECT_CALL(mock_, serialPrintln(HasSubstr("FAST"))).Times(1);
|
||||
app.update();
|
||||
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
EXPECT_EQ(app.interval(), BlinkApp::FAST_INTERVAL_MS);
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, SecondButtonPressReturnsToSlowMode) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// First press: fast mode
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Release
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.update();
|
||||
|
||||
// Second press: back to slow
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
EXPECT_CALL(mock_, serialPrintln(HasSubstr("SLOW"))).Times(1);
|
||||
app.update();
|
||||
|
||||
EXPECT_FALSE(app.fastMode());
|
||||
}
|
||||
|
||||
TEST_F(BlinkAppUnitTest, HoldingButtonDoesNotRepeatToggle) {
|
||||
BlinkApp app(&mock_, 13, 2);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(HIGH));
|
||||
app.begin();
|
||||
|
||||
// Press and hold
|
||||
ON_CALL(mock_, digitalRead(2)).WillByDefault(Return(LOW));
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode());
|
||||
|
||||
// Still held -- should NOT toggle again
|
||||
app.update();
|
||||
app.update();
|
||||
EXPECT_TRUE(app.fastMode()); // still fast, not toggled back
|
||||
}
|
||||
140
test/test_i2c_example.cpp
Normal file
140
test/test_i2c_example.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
// ============================================================================
|
||||
// Example: I2C Temperature Sensor Simulator
|
||||
//
|
||||
// Demonstrates how to mock an I2C device. This simulates a simple
|
||||
// temperature sensor at address 0x48 (like a TMP102 or LM75).
|
||||
//
|
||||
// Protocol (simplified):
|
||||
// Write register pointer (1 byte), then read 2 bytes back.
|
||||
// Register 0x00 = temperature (MSB:LSB, 12-bit, 0.0625 deg/LSB)
|
||||
// Register 0x01 = config register
|
||||
// ============================================================================
|
||||
|
||||
class TempSensorSim : public I2cDeviceSim {
|
||||
public:
|
||||
static const uint8_t ADDR = 0x48;
|
||||
static const uint8_t REG_TEMP = 0x00;
|
||||
static const uint8_t REG_CONFIG = 0x01;
|
||||
|
||||
TempSensorSim() : reg_pointer_(0), temp_raw_(0) {}
|
||||
|
||||
// Set temperature in degrees C (converted to 12-bit raw)
|
||||
void setTemperature(float deg_c) {
|
||||
temp_raw_ = static_cast<int16_t>(deg_c / 0.0625f);
|
||||
}
|
||||
|
||||
// I2cDeviceSim interface
|
||||
void onReceive(const uint8_t* data, size_t len) override {
|
||||
if (len >= 1) {
|
||||
reg_pointer_ = data[0];
|
||||
}
|
||||
}
|
||||
|
||||
size_t onRequest(uint8_t* buf, size_t max_len) override {
|
||||
if (max_len < 2) return 0;
|
||||
|
||||
if (reg_pointer_ == REG_TEMP) {
|
||||
// 12-bit left-aligned in 16 bits
|
||||
int16_t raw = temp_raw_ << 4;
|
||||
buf[0] = static_cast<uint8_t>((raw >> 8) & 0xFF);
|
||||
buf[1] = static_cast<uint8_t>(raw & 0xFF);
|
||||
return 2;
|
||||
}
|
||||
if (reg_pointer_ == REG_CONFIG) {
|
||||
buf[0] = 0x60; // default config
|
||||
buf[1] = 0xA0;
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t reg_pointer_;
|
||||
int16_t temp_raw_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helper: read temperature the way real Arduino code would
|
||||
// -----------------------------------------------------------------------
|
||||
static float readTemperature(Hal* hal, uint8_t addr) {
|
||||
// Set register pointer to 0x00 (temperature)
|
||||
hal->i2cBeginTransmission(addr);
|
||||
hal->i2cWrite(TempSensorSim::REG_TEMP);
|
||||
hal->i2cEndTransmission();
|
||||
|
||||
// Read 2 bytes
|
||||
uint8_t count = hal->i2cRequestFrom(addr, 2);
|
||||
if (count < 2) return -999.0f;
|
||||
|
||||
uint8_t msb = static_cast<uint8_t>(hal->i2cRead());
|
||||
uint8_t lsb = static_cast<uint8_t>(hal->i2cRead());
|
||||
|
||||
// Convert 12-bit left-aligned to temperature
|
||||
int16_t raw = (static_cast<int16_t>(msb) << 8) | lsb;
|
||||
raw >>= 4; // right-align the 12 bits
|
||||
return raw * 0.0625f;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
class I2cTempSensorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
sensor_.setTemperature(25.0f);
|
||||
sim_.attachI2cDevice(TempSensorSim::ADDR, &sensor_);
|
||||
sim_.i2cBegin();
|
||||
}
|
||||
|
||||
SimHal sim_;
|
||||
TempSensorSim sensor_;
|
||||
};
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsRoomTemperature) {
|
||||
sensor_.setTemperature(25.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 25.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsFreezing) {
|
||||
sensor_.setTemperature(0.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 0.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsNegativeTemperature) {
|
||||
sensor_.setTemperature(-10.5f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, -10.5f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, ReadsHighTemperature) {
|
||||
sensor_.setTemperature(85.0f);
|
||||
float temp = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
EXPECT_NEAR(temp, 85.0f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, UnregisteredDeviceReturnsNack) {
|
||||
// Try to talk to a device that does not exist
|
||||
sim_.i2cBeginTransmission(0x50);
|
||||
sim_.i2cWrite(0x00);
|
||||
uint8_t result = sim_.i2cEndTransmission();
|
||||
EXPECT_NE(result, 0); // non-zero = error
|
||||
}
|
||||
|
||||
TEST_F(I2cTempSensorTest, TemperatureUpdatesLive) {
|
||||
sensor_.setTemperature(20.0f);
|
||||
float t1 = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
|
||||
sensor_.setTemperature(30.0f);
|
||||
float t2 = readTemperature(&sim_, TempSensorSim::ADDR);
|
||||
|
||||
EXPECT_NEAR(t1, 20.0f, 0.1f);
|
||||
EXPECT_NEAR(t2, 30.0f, 0.1f);
|
||||
}
|
||||
Reference in New Issue
Block a user