Anvil v1.0.0 -- Arduino build tool with HAL and test scaffolding
Single-binary CLI that scaffolds testable Arduino projects, compiles, uploads, and monitors serial output. Templates embed a hardware abstraction layer, Google Mock infrastructure, and CMake-based host tests so application logic can be verified without hardware. Commands: new, doctor, setup, devices, build, upload, monitor 39 Rust tests (21 unit, 18 integration) Cross-platform: Linux and Windows
This commit is contained in:
74
templates/basic/README.md.tmpl
Normal file
74
templates/basic/README.md.tmpl
Normal file
@@ -0,0 +1,74 @@
|
||||
# {{PROJECT_NAME}}
|
||||
|
||||
Arduino project generated by Anvil v{{ANVIL_VERSION}}.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Check your system
|
||||
anvil doctor
|
||||
|
||||
# Find connected boards
|
||||
anvil devices
|
||||
|
||||
# Compile only (no upload)
|
||||
anvil build --verify {{PROJECT_NAME}}
|
||||
|
||||
# Compile and upload
|
||||
anvil build {{PROJECT_NAME}}
|
||||
|
||||
# Compile, upload, and open serial monitor
|
||||
anvil build --monitor {{PROJECT_NAME}}
|
||||
|
||||
# Run host-side unit tests (no board needed)
|
||||
cd test && ./run_tests.sh
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{{PROJECT_NAME}}/
|
||||
{{PROJECT_NAME}}/
|
||||
{{PROJECT_NAME}}.ino Entry point (setup + loop)
|
||||
lib/
|
||||
hal/
|
||||
hal.h Hardware abstraction interface
|
||||
hal_arduino.h Real hardware implementation
|
||||
app/
|
||||
{{PROJECT_NAME}}_app.h Application logic (testable)
|
||||
test/
|
||||
mocks/
|
||||
mock_hal.h Google Mock HAL
|
||||
sim_hal.h Stateful simulator HAL
|
||||
test_unit.cpp Unit tests
|
||||
CMakeLists.txt Test build system
|
||||
run_tests.sh Test runner (Linux/Mac)
|
||||
run_tests.bat Test runner (Windows)
|
||||
.anvil.toml Project configuration
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
All hardware access goes through the `Hal` interface. The app code
|
||||
(`lib/app/`) depends only on `Hal`, never on `Arduino.h` directly.
|
||||
This means the app can be compiled and tested on the host without
|
||||
any Arduino SDK.
|
||||
|
||||
Two HAL implementations:
|
||||
- `ArduinoHal` -- passthroughs to real hardware (used in the .ino)
|
||||
- `MockHal` -- Google Mock for verifying exact call sequences in tests
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `.anvil.toml` to change board, baud rate, or build settings:
|
||||
|
||||
```toml
|
||||
[build]
|
||||
fqbn = "arduino:avr:uno"
|
||||
warnings = "more"
|
||||
include_dirs = ["lib/hal", "lib/app"]
|
||||
extra_flags = ["-Werror"]
|
||||
|
||||
[monitor]
|
||||
baud = 115200
|
||||
```
|
||||
28
templates/basic/__name__/__name__.ino.tmpl
Normal file
28
templates/basic/__name__/__name__.ino.tmpl
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* {{PROJECT_NAME}}.ino -- LED blink with button-controlled speed
|
||||
*
|
||||
* This .ino file is the entry point. All logic lives in the app
|
||||
* header (lib/app/{{PROJECT_NAME}}_app.h) which depends on the HAL
|
||||
* interface (lib/hal/hal.h), making it testable on the host.
|
||||
*
|
||||
* Wiring:
|
||||
* Pin 13 (LED_BUILTIN) -- onboard LED (no wiring needed)
|
||||
* Pin 2 -- momentary button to GND (uses INPUT_PULLUP)
|
||||
*
|
||||
* Serial: 115200 baud
|
||||
* Prints "FAST" or "SLOW" on button press.
|
||||
*/
|
||||
|
||||
#include <hal_arduino.h>
|
||||
#include <{{PROJECT_NAME}}_app.h>
|
||||
|
||||
static ArduinoHal hw;
|
||||
static BlinkApp app(&hw);
|
||||
|
||||
void setup() {
|
||||
app.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
app.update();
|
||||
}
|
||||
12
templates/basic/_dot_anvil.toml.tmpl
Normal file
12
templates/basic/_dot_anvil.toml.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "{{PROJECT_NAME}}"
|
||||
anvil_version = "{{ANVIL_VERSION}}"
|
||||
|
||||
[build]
|
||||
fqbn = "arduino:avr:uno"
|
||||
warnings = "more"
|
||||
include_dirs = ["lib/hal", "lib/app"]
|
||||
extra_flags = ["-Werror"]
|
||||
|
||||
[monitor]
|
||||
baud = 115200
|
||||
8
templates/basic/_dot_clang-format
Normal file
8
templates/basic/_dot_clang-format
Normal file
@@ -0,0 +1,8 @@
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
BreakBeforeBraces: Attach
|
||||
PointerAlignment: Left
|
||||
SortIncludes: false
|
||||
21
templates/basic/_dot_editorconfig
Normal file
21
templates/basic/_dot_editorconfig
Normal file
@@ -0,0 +1,21 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{h,cpp,ino}]
|
||||
indent_size = 4
|
||||
|
||||
[CMakeLists.txt]
|
||||
indent_size = 4
|
||||
|
||||
[*.{sh,bat}]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
10
templates/basic/_dot_gitignore
Normal file
10
templates/basic/_dot_gitignore
Normal file
@@ -0,0 +1,10 @@
|
||||
# Build artifacts
|
||||
test/build/
|
||||
|
||||
# IDE
|
||||
.vscode/.browse*
|
||||
.vscode/*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
21
templates/basic/_dot_vscode/settings.json
Normal file
21
templates/basic/_dot_vscode/settings.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.ino": "cpp",
|
||||
"*.h": "cpp"
|
||||
},
|
||||
"C_Cpp.default.includePath": [
|
||||
"${workspaceFolder}/lib/hal",
|
||||
"${workspaceFolder}/lib/app"
|
||||
],
|
||||
"C_Cpp.default.defines": [
|
||||
"LED_BUILTIN=13",
|
||||
"INPUT=0x0",
|
||||
"OUTPUT=0x1",
|
||||
"INPUT_PULLUP=0x2",
|
||||
"LOW=0x0",
|
||||
"HIGH=0x1"
|
||||
],
|
||||
"editor.formatOnSave": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true
|
||||
}
|
||||
88
templates/basic/lib/app/__name___app.h.tmpl
Normal file
88
templates/basic/lib/app/__name___app.h.tmpl
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef APP_H
|
||||
#define APP_H
|
||||
|
||||
#include <hal.h>
|
||||
|
||||
/*
|
||||
* BlinkApp -- Testable blink logic, decoupled from hardware.
|
||||
*
|
||||
* Blinks an LED and reads a button. When the button is pressed,
|
||||
* the blink rate doubles (toggles between normal and fast mode).
|
||||
*
|
||||
* All hardware access goes through the injected Hal pointer. This
|
||||
* class has no dependency on Arduino.h and compiles on any host.
|
||||
*/
|
||||
class BlinkApp {
|
||||
public:
|
||||
static constexpr uint8_t DEFAULT_LED_PIN = LED_BUILTIN; // pin 13
|
||||
static constexpr uint8_t DEFAULT_BUTTON_PIN = 2;
|
||||
static constexpr unsigned long SLOW_INTERVAL_MS = 500;
|
||||
static constexpr unsigned long FAST_INTERVAL_MS = 125;
|
||||
|
||||
BlinkApp(Hal* hal,
|
||||
uint8_t led_pin = DEFAULT_LED_PIN,
|
||||
uint8_t button_pin = DEFAULT_BUTTON_PIN)
|
||||
: hal_(hal)
|
||||
, led_pin_(led_pin)
|
||||
, button_pin_(button_pin)
|
||||
, led_state_(LOW)
|
||||
, fast_mode_(false)
|
||||
, last_toggle_ms_(0)
|
||||
, last_button_state_(HIGH) // pulled up, so HIGH = not pressed
|
||||
{}
|
||||
|
||||
// Call once from setup()
|
||||
void begin() {
|
||||
hal_->pinMode(led_pin_, OUTPUT);
|
||||
hal_->pinMode(button_pin_, INPUT_PULLUP);
|
||||
hal_->serialBegin(115200);
|
||||
hal_->serialPrintln("BlinkApp started");
|
||||
last_toggle_ms_ = hal_->millis();
|
||||
}
|
||||
|
||||
// Call repeatedly from loop()
|
||||
void update() {
|
||||
handleButton();
|
||||
handleBlink();
|
||||
}
|
||||
|
||||
// -- Accessors for testing ----------------------------------------------
|
||||
bool ledState() const { return led_state_ == HIGH; }
|
||||
bool fastMode() const { return fast_mode_; }
|
||||
unsigned long interval() const {
|
||||
return fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
|
||||
}
|
||||
|
||||
private:
|
||||
void handleButton() {
|
||||
uint8_t reading = hal_->digitalRead(button_pin_);
|
||||
|
||||
// Detect falling edge (HIGH -> LOW = button press with INPUT_PULLUP)
|
||||
if (last_button_state_ == HIGH && reading == LOW) {
|
||||
fast_mode_ = !fast_mode_;
|
||||
hal_->serialPrintln(fast_mode_ ? "FAST" : "SLOW");
|
||||
}
|
||||
last_button_state_ = reading;
|
||||
}
|
||||
|
||||
void handleBlink() {
|
||||
unsigned long now = hal_->millis();
|
||||
unsigned long target = fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
|
||||
|
||||
if (now - last_toggle_ms_ >= target) {
|
||||
led_state_ = (led_state_ == HIGH) ? LOW : HIGH;
|
||||
hal_->digitalWrite(led_pin_, led_state_);
|
||||
last_toggle_ms_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
Hal* hal_;
|
||||
uint8_t led_pin_;
|
||||
uint8_t button_pin_;
|
||||
uint8_t led_state_;
|
||||
bool fast_mode_;
|
||||
unsigned long last_toggle_ms_;
|
||||
uint8_t last_button_state_;
|
||||
};
|
||||
|
||||
#endif // APP_H
|
||||
67
templates/basic/lib/hal/hal.h
Normal file
67
templates/basic/lib/hal/hal.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef HAL_H
|
||||
#define HAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Pin modes (match Arduino constants)
|
||||
#ifndef INPUT
|
||||
#define INPUT 0x0
|
||||
#define OUTPUT 0x1
|
||||
#define INPUT_PULLUP 0x2
|
||||
#endif
|
||||
|
||||
#ifndef LOW
|
||||
#define LOW 0x0
|
||||
#define HIGH 0x1
|
||||
#endif
|
||||
|
||||
// LED_BUILTIN for host builds
|
||||
#ifndef LED_BUILTIN
|
||||
#define LED_BUILTIN 13
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Hardware Abstraction Layer
|
||||
*
|
||||
* Abstract interface over GPIO, timing, serial, and I2C. Sketch logic
|
||||
* depends on this interface only -- never on Arduino.h directly.
|
||||
*
|
||||
* Two implementations:
|
||||
* hal_arduino.h -- real hardware (included by .ino files)
|
||||
* mock_hal.h -- Google Mock (included by test files)
|
||||
*/
|
||||
class Hal {
|
||||
public:
|
||||
virtual ~Hal() = default;
|
||||
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
virtual void pinMode(uint8_t pin, uint8_t mode) = 0;
|
||||
virtual void digitalWrite(uint8_t pin, uint8_t value) = 0;
|
||||
virtual uint8_t digitalRead(uint8_t pin) = 0;
|
||||
virtual int analogRead(uint8_t pin) = 0;
|
||||
virtual void analogWrite(uint8_t pin, int value) = 0;
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
virtual unsigned long millis() = 0;
|
||||
virtual unsigned long micros() = 0;
|
||||
virtual void delay(unsigned long ms) = 0;
|
||||
virtual void delayMicroseconds(unsigned long us) = 0;
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
virtual void serialBegin(unsigned long baud) = 0;
|
||||
virtual void serialPrint(const char* msg) = 0;
|
||||
virtual void serialPrintln(const char* msg) = 0;
|
||||
virtual int serialAvailable() = 0;
|
||||
virtual int serialRead() = 0;
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
virtual void i2cBegin() = 0;
|
||||
virtual void i2cBeginTransmission(uint8_t addr) = 0;
|
||||
virtual size_t i2cWrite(uint8_t data) = 0;
|
||||
virtual uint8_t i2cEndTransmission() = 0;
|
||||
virtual uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) = 0;
|
||||
virtual int i2cAvailable() = 0;
|
||||
virtual int i2cRead() = 0;
|
||||
};
|
||||
|
||||
#endif // HAL_H
|
||||
93
templates/basic/lib/hal/hal_arduino.h
Normal file
93
templates/basic/lib/hal/hal_arduino.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef HAL_ARDUINO_H
|
||||
#define HAL_ARDUINO_H
|
||||
|
||||
/*
|
||||
* Real hardware implementation of the HAL.
|
||||
*
|
||||
* This file includes Arduino.h and Wire.h, so it can only be compiled
|
||||
* by avr-gcc (via arduino-cli). It is included by .ino files only.
|
||||
*
|
||||
* Every method is a trivial passthrough to the real Arduino function.
|
||||
* The point is not to add logic here -- it is to keep Arduino.h out
|
||||
* of your application code so that code can compile on the host.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <hal.h>
|
||||
|
||||
class ArduinoHal : public Hal {
|
||||
public:
|
||||
// -- GPIO ---------------------------------------------------------------
|
||||
void pinMode(uint8_t pin, uint8_t mode) override {
|
||||
::pinMode(pin, mode);
|
||||
}
|
||||
void digitalWrite(uint8_t pin, uint8_t value) override {
|
||||
::digitalWrite(pin, value);
|
||||
}
|
||||
uint8_t digitalRead(uint8_t pin) override {
|
||||
return ::digitalRead(pin);
|
||||
}
|
||||
int analogRead(uint8_t pin) override {
|
||||
return ::analogRead(pin);
|
||||
}
|
||||
void analogWrite(uint8_t pin, int value) override {
|
||||
::analogWrite(pin, value);
|
||||
}
|
||||
|
||||
// -- Timing -------------------------------------------------------------
|
||||
unsigned long millis() override {
|
||||
return ::millis();
|
||||
}
|
||||
unsigned long micros() override {
|
||||
return ::micros();
|
||||
}
|
||||
void delay(unsigned long ms) override {
|
||||
::delay(ms);
|
||||
}
|
||||
void delayMicroseconds(unsigned long us) override {
|
||||
::delayMicroseconds(us);
|
||||
}
|
||||
|
||||
// -- Serial -------------------------------------------------------------
|
||||
void serialBegin(unsigned long baud) override {
|
||||
Serial.begin(baud);
|
||||
}
|
||||
void serialPrint(const char* msg) override {
|
||||
Serial.print(msg);
|
||||
}
|
||||
void serialPrintln(const char* msg) override {
|
||||
Serial.println(msg);
|
||||
}
|
||||
int serialAvailable() override {
|
||||
return Serial.available();
|
||||
}
|
||||
int serialRead() override {
|
||||
return Serial.read();
|
||||
}
|
||||
|
||||
// -- I2C ----------------------------------------------------------------
|
||||
void i2cBegin() override {
|
||||
Wire.begin();
|
||||
}
|
||||
void i2cBeginTransmission(uint8_t addr) override {
|
||||
Wire.beginTransmission(addr);
|
||||
}
|
||||
size_t i2cWrite(uint8_t data) override {
|
||||
return Wire.write(data);
|
||||
}
|
||||
uint8_t i2cEndTransmission() override {
|
||||
return Wire.endTransmission();
|
||||
}
|
||||
uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) override {
|
||||
return Wire.requestFrom(addr, count);
|
||||
}
|
||||
int i2cAvailable() override {
|
||||
return Wire.available();
|
||||
}
|
||||
int i2cRead() override {
|
||||
return Wire.read();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HAL_ARDUINO_H
|
||||
47
templates/basic/test/CMakeLists.txt.tmpl
Normal file
47
templates/basic/test/CMakeLists.txt.tmpl
Normal file
@@ -0,0 +1,47 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project({{PROJECT_NAME}}_tests LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Google Test (fetched automatically on first build)
|
||||
# --------------------------------------------------------------------------
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG v1.14.0
|
||||
)
|
||||
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
enable_testing()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Include paths -- same headers the Arduino sketch uses
|
||||
# --------------------------------------------------------------------------
|
||||
set(LIB_DIR ${CMAKE_SOURCE_DIR}/../lib)
|
||||
|
||||
include_directories(
|
||||
${LIB_DIR}/hal
|
||||
${LIB_DIR}/app
|
||||
${CMAKE_SOURCE_DIR}/mocks
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Unit tests (Google Mock)
|
||||
# --------------------------------------------------------------------------
|
||||
add_executable(test_unit
|
||||
test_unit.cpp
|
||||
)
|
||||
target_link_libraries(test_unit
|
||||
GTest::gtest_main
|
||||
GTest::gmock
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Register with CTest
|
||||
# --------------------------------------------------------------------------
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(test_unit)
|
||||
45
templates/basic/test/mocks/mock_hal.h
Normal file
45
templates/basic/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
templates/basic/test/mocks/sim_hal.h
Normal file
256
templates/basic/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
|
||||
42
templates/basic/test/run_tests.bat
Normal file
42
templates/basic/test/run_tests.bat
Normal file
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
set BUILD_DIR=%SCRIPT_DIR%build
|
||||
|
||||
if "%1"=="--clean" (
|
||||
if exist "%BUILD_DIR%" (
|
||||
echo Cleaning build directory...
|
||||
rmdir /s /q "%BUILD_DIR%"
|
||||
)
|
||||
)
|
||||
|
||||
if not exist "%BUILD_DIR%\CMakeCache.txt" (
|
||||
echo Configuring (first run will fetch Google Test)...
|
||||
cmake -S "%SCRIPT_DIR%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Debug
|
||||
if errorlevel 1 (
|
||||
echo FAIL: cmake configure failed
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Building tests...
|
||||
cmake --build "%BUILD_DIR%" --parallel
|
||||
if errorlevel 1 (
|
||||
echo FAIL: build failed
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Running tests...
|
||||
echo.
|
||||
|
||||
ctest --test-dir "%BUILD_DIR%" --output-on-failure
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo FAIL: Some tests failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo PASS: All tests passed.
|
||||
73
templates/basic/test/run_tests.sh
Normal file
73
templates/basic/test/run_tests.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run_tests.sh -- Build and run host-side unit 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
|
||||
#
|
||||
# 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; }
|
||||
|
||||
DO_CLEAN=0
|
||||
VERBOSE=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--clean) DO_CLEAN=1 ;;
|
||||
--verbose) VERBOSE="--verbose" ;;
|
||||
*) die "Unknown option: $arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
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)"
|
||||
|
||||
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 (first run will fetch Google Test)..."
|
||||
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
|
||||
fi
|
||||
|
||||
info "Building tests..."
|
||||
cmake --build "$BUILD_DIR" --parallel
|
||||
|
||||
echo ""
|
||||
info "${BLD}Running tests...${RST}"
|
||||
echo ""
|
||||
|
||||
CTEST_ARGS=("--test-dir" "$BUILD_DIR" "--output-on-failure")
|
||||
[[ -n "$VERBOSE" ]] && CTEST_ARGS+=("--verbose")
|
||||
|
||||
if ctest "${CTEST_ARGS[@]}"; then
|
||||
echo ""
|
||||
ok "${BLD}All tests passed.${RST}"
|
||||
else
|
||||
echo ""
|
||||
die "Some tests failed."
|
||||
fi
|
||||
124
templates/basic/test/test_unit.cpp.tmpl
Normal file
124
templates/basic/test/test_unit.cpp.tmpl
Normal file
@@ -0,0 +1,124 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "mock_hal.h"
|
||||
#include "{{PROJECT_NAME}}_app.h"
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AnyNumber;
|
||||
using ::testing::Return;
|
||||
using ::testing::HasSubstr;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests -- verify exact HAL interactions
|
||||
// ============================================================================
|
||||
|
||||
class BlinkAppUnitTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
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_);
|
||||
|
||||
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||
app.begin();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user