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:
Eric Ratliff
2026-02-14 09:25:49 -06:00
committed by Eric Ratliff
commit 61f4659462
18 changed files with 2417 additions and 0 deletions

132
test/test_blink_unit.cpp Normal file
View 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
}