New button template
This commit is contained in:
11
README.md
11
README.md
@@ -63,11 +63,14 @@ point, use a composed template:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
anvil new weather_station --template weather --board uno
|
anvil new weather_station --template weather --board uno
|
||||||
|
anvil new clicker --template button --board uno
|
||||||
```
|
```
|
||||||
|
|
||||||
The weather template builds on basic, adding a `WeatherApp` with a TMP36
|
The **weather** template adds a `WeatherApp` with a TMP36 temperature sensor,
|
||||||
temperature sensor driver, managed example tests demonstrating both mock and
|
managed example tests, and student test starters. The **button** template adds
|
||||||
simulator patterns, and student test starters. To see available templates:
|
a `ButtonApp` with edge detection that prints "Button pressed!" to the serial
|
||||||
|
monitor each time you press a button -- no repeated messages from holding it
|
||||||
|
down. Both templates include mock and simulator patterns. To see all options:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
anvil new --list-templates
|
anvil new --list-templates
|
||||||
@@ -437,7 +440,7 @@ Upload these to a Gitea release. The script requires `build-essential`,
|
|||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
615 tests (137 unit + 478 integration), ~4 seconds, zero warnings.
|
642 tests (137 unit + 505 integration), ~4 seconds, zero warnings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::version::ANVIL_VERSION;
|
|||||||
// Embedded template directories
|
// Embedded template directories
|
||||||
static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic");
|
static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic");
|
||||||
static WEATHER_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/weather");
|
static WEATHER_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/weather");
|
||||||
|
static BUTTON_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/button");
|
||||||
|
|
||||||
/// Context variables available in .tmpl files via {{VAR}} substitution.
|
/// Context variables available in .tmpl files via {{VAR}} substitution.
|
||||||
pub struct TemplateContext {
|
pub struct TemplateContext {
|
||||||
@@ -106,13 +107,17 @@ fn template_dir(name: &str) -> Option<&'static Dir<'static>> {
|
|||||||
match name {
|
match name {
|
||||||
"basic" => Some(&BASIC_TEMPLATE),
|
"basic" => Some(&BASIC_TEMPLATE),
|
||||||
"weather" => Some(&WEATHER_TEMPLATE),
|
"weather" => Some(&WEATHER_TEMPLATE),
|
||||||
|
"button" => Some(&BUTTON_TEMPLATE),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All composed templates (everything except "basic").
|
// All composed templates (everything except "basic").
|
||||||
fn composed_template_entries() -> Vec<(&'static str, &'static Dir<'static>)> {
|
fn composed_template_entries() -> Vec<(&'static str, &'static Dir<'static>)> {
|
||||||
vec![("weather", &WEATHER_TEMPLATE)]
|
vec![
|
||||||
|
("weather", &WEATHER_TEMPLATE),
|
||||||
|
("button", &BUTTON_TEMPLATE),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
35
templates/button/__name__/__name__.ino.tmpl
Normal file
35
templates/button/__name__/__name__.ino.tmpl
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* {{PROJECT_NAME}}.ino -- Pushbutton input with edge detection
|
||||||
|
*
|
||||||
|
* Detects button presses (rising edge only) and prints to Serial:
|
||||||
|
*
|
||||||
|
* Button pressed! (count: 1)
|
||||||
|
* Button pressed! (count: 2)
|
||||||
|
*
|
||||||
|
* All logic lives in lib/app/{{PROJECT_NAME}}_app.h which depends
|
||||||
|
* on the HAL and Button interfaces, making it fully testable
|
||||||
|
* on the host without hardware.
|
||||||
|
*
|
||||||
|
* Wiring (active-low, no external resistor needed):
|
||||||
|
* Pin 2 -> one leg of the button
|
||||||
|
* GND -> other leg of the button
|
||||||
|
* (uses INPUT_PULLUP internally)
|
||||||
|
*
|
||||||
|
* Serial: 115200 baud
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <hal_arduino.h>
|
||||||
|
#include <{{PROJECT_NAME}}_app.h>
|
||||||
|
#include <button_digital.h>
|
||||||
|
|
||||||
|
static ArduinoHal hw;
|
||||||
|
static ButtonDigital btn(&hw, 2); // pin 2, active-low (default)
|
||||||
|
static ButtonApp app(&hw, &btn);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
app.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
77
templates/button/lib/app/__name___app.h.tmpl
Normal file
77
templates/button/lib/app/__name___app.h.tmpl
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#ifndef APP_H
|
||||||
|
#define APP_H
|
||||||
|
|
||||||
|
#include <hal.h>
|
||||||
|
#include "button.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ButtonApp -- Detects button presses and reports to Serial.
|
||||||
|
*
|
||||||
|
* Uses polling with edge detection: only triggers on the transition
|
||||||
|
* from released to pressed (rising edge). Holding the button does
|
||||||
|
* not generate repeated messages.
|
||||||
|
*
|
||||||
|
* Each press prints:
|
||||||
|
* Button pressed! (count: 1)
|
||||||
|
*
|
||||||
|
* The button is injected through the Button interface, so this
|
||||||
|
* class works with real hardware, mocks, or simulations.
|
||||||
|
*
|
||||||
|
* Wiring (active-low, no external resistor needed):
|
||||||
|
* Digital pin 2 -> one leg of the button
|
||||||
|
* GND -> other leg of the button
|
||||||
|
* Set pin mode to INPUT_PULLUP in begin().
|
||||||
|
*/
|
||||||
|
class ButtonApp {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t BUTTON_PIN = 2;
|
||||||
|
|
||||||
|
ButtonApp(Hal* hal, Button* button)
|
||||||
|
: hal_(hal)
|
||||||
|
, button_(button)
|
||||||
|
, was_pressed_(false)
|
||||||
|
, press_count_(0)
|
||||||
|
{}
|
||||||
|
|
||||||
|
// Call once from setup()
|
||||||
|
void begin() {
|
||||||
|
hal_->serialBegin(115200);
|
||||||
|
hal_->pinMode(BUTTON_PIN, INPUT_PULLUP);
|
||||||
|
hal_->serialPrintln("ButtonApp ready -- press the button!");
|
||||||
|
// Read initial state so we don't false-trigger on startup
|
||||||
|
was_pressed_ = button_->isPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call repeatedly from loop()
|
||||||
|
void update() {
|
||||||
|
bool pressed_now = button_->isPressed();
|
||||||
|
|
||||||
|
// Rising edge: was released, now pressed
|
||||||
|
if (pressed_now && !was_pressed_) {
|
||||||
|
press_count_++;
|
||||||
|
onPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
was_pressed_ = pressed_now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Accessors for testing ------------------------------------------------
|
||||||
|
int pressCount() const { return press_count_; }
|
||||||
|
bool wasPressed() const { return was_pressed_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onPress() {
|
||||||
|
char buf[40];
|
||||||
|
snprintf(buf, sizeof(buf), "Button pressed! (count: %d)", press_count_);
|
||||||
|
hal_->serialPrintln(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
Hal* hal_;
|
||||||
|
Button* button_;
|
||||||
|
bool was_pressed_;
|
||||||
|
int press_count_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // APP_H
|
||||||
25
templates/button/template.toml
Normal file
25
templates/button/template.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[template]
|
||||||
|
name = "button"
|
||||||
|
base = "basic"
|
||||||
|
description = "Pushbutton input with edge detection and serial output"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
libraries = ["button"]
|
||||||
|
board_capabilities = ["digital"]
|
||||||
|
|
||||||
|
# Default pin assignments per board.
|
||||||
|
# Pin 2 supports interrupts on most boards, good default for buttons.
|
||||||
|
[pins.default]
|
||||||
|
button_signal = { pin = "2", mode = "input" }
|
||||||
|
|
||||||
|
[pins.uno]
|
||||||
|
button_signal = { pin = "2", mode = "input" }
|
||||||
|
|
||||||
|
[pins.mega]
|
||||||
|
button_signal = { pin = "2", mode = "input" }
|
||||||
|
|
||||||
|
[pins.nano]
|
||||||
|
button_signal = { pin = "2", mode = "input" }
|
||||||
|
|
||||||
|
[pins.leonardo]
|
||||||
|
button_signal = { pin = "2", mode = "input" }
|
||||||
283
templates/button/test/test_button_app.cpp.tmpl
Normal file
283
templates/button/test/test_button_app.cpp.tmpl
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
/*
|
||||||
|
* test_button_app.cpp -- Button application example tests.
|
||||||
|
*
|
||||||
|
* THIS FILE IS MANAGED BY ANVIL and will be updated by `anvil refresh`.
|
||||||
|
* Do not edit -- put your own tests in test_unit.cpp and test_system.cpp.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
|
||||||
|
#include "mock_arduino.h"
|
||||||
|
#include "hal.h"
|
||||||
|
#include "mock_hal.h"
|
||||||
|
#include "sim_hal.h"
|
||||||
|
#include "button_mock.h"
|
||||||
|
#include "button_sim.h"
|
||||||
|
#include "{{PROJECT_NAME}}_app.h"
|
||||||
|
|
||||||
|
using ::testing::_;
|
||||||
|
using ::testing::AnyNumber;
|
||||||
|
using ::testing::Return;
|
||||||
|
using ::testing::HasSubstr;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Unit Tests -- verify ButtonApp behavior with mock button
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class ButtonUnitTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ON_CALL(mock_, millis()).WillByDefault(Return(0));
|
||||||
|
EXPECT_CALL(mock_, serialBegin(_)).Times(AnyNumber());
|
||||||
|
EXPECT_CALL(mock_, serialPrint(_)).Times(AnyNumber());
|
||||||
|
EXPECT_CALL(mock_, serialPrintln(_)).Times(AnyNumber());
|
||||||
|
EXPECT_CALL(mock_, millis()).Times(AnyNumber());
|
||||||
|
EXPECT_CALL(mock_, pinMode(_, _)).Times(AnyNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::NiceMock<MockHal> mock_;
|
||||||
|
ButtonMock btn_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, BeginPrintsReadyMessage) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
|
||||||
|
EXPECT_CALL(mock_, serialBegin(115200)).Times(1);
|
||||||
|
EXPECT_CALL(mock_, serialPrintln(HasSubstr("ButtonApp ready"))).Times(1);
|
||||||
|
|
||||||
|
app.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, BeginSetsPinMode) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
|
||||||
|
EXPECT_CALL(mock_, pinMode(2, INPUT_PULLUP)).Times(1);
|
||||||
|
|
||||||
|
app.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, NoPressNoCount) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
// Button not pressed, update should not increment count
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.update();
|
||||||
|
app.update();
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, SinglePressIncrementsCount) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, HoldDoesNotRepeat) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update(); // rising edge -> count = 1
|
||||||
|
app.update(); // still pressed -> no increment
|
||||||
|
app.update(); // still pressed -> no increment
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, ReleaseAndPressAgain) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
// First press
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
|
||||||
|
// Release
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Second press
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
EXPECT_EQ(app.pressCount(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, PrintsMessageOnPress) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
EXPECT_CALL(mock_, serialPrintln(HasSubstr("Button pressed!"))).Times(1);
|
||||||
|
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, PrintsCountInMessage) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
EXPECT_CALL(mock_, serialPrintln(HasSubstr("count: 1"))).Times(1);
|
||||||
|
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, DoesNotPrintOnRelease) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Release should not trigger another print
|
||||||
|
EXPECT_CALL(mock_, serialPrintln(HasSubstr("Button pressed!"))).Times(0);
|
||||||
|
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, TenPresses) {
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonUnitTest, StartupWithButtonAlreadyPressed) {
|
||||||
|
// If the button is held during startup, begin() reads it as pressed.
|
||||||
|
// The first update() should NOT trigger a press (no rising edge).
|
||||||
|
btn_.setPressed(true);
|
||||||
|
|
||||||
|
ButtonApp app(&mock_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
app.update(); // still pressed, no edge
|
||||||
|
EXPECT_EQ(app.pressCount(), 0);
|
||||||
|
|
||||||
|
// Release and press again -- THIS should trigger
|
||||||
|
btn_.setPressed(false);
|
||||||
|
app.update();
|
||||||
|
btn_.setPressed(true);
|
||||||
|
app.update();
|
||||||
|
EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// System Tests -- exercise ButtonApp with simulated button and hardware
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class ButtonSystemTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
mock_arduino_reset();
|
||||||
|
sim_.setMillis(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SimHal sim_;
|
||||||
|
ButtonSim btn_{0}; // no bounce for predictable tests
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, StartsAndPrintsToSerial) {
|
||||||
|
ButtonApp app(&sim_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
std::string output = sim_.serialOutput();
|
||||||
|
EXPECT_NE(output.find("ButtonApp ready"), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, PressShowsInSerialOutput) {
|
||||||
|
ButtonApp app(&sim_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
btn_.press();
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
std::string output = sim_.serialOutput();
|
||||||
|
EXPECT_NE(output.find("Button pressed!"), std::string::npos);
|
||||||
|
EXPECT_NE(output.find("count: 1"), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, MultiplePressesCountCorrectly) {
|
||||||
|
ButtonApp app(&sim_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
btn_.press();
|
||||||
|
app.update();
|
||||||
|
btn_.release();
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 5);
|
||||||
|
|
||||||
|
std::string output = sim_.serialOutput();
|
||||||
|
EXPECT_NE(output.find("count: 5"), std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, HoldOnlyCountsOnce) {
|
||||||
|
ButtonApp app(&sim_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
btn_.press();
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, RapidPressReleaseCycle) {
|
||||||
|
ButtonApp app(&sim_, &btn_);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
// Simulate rapid button mashing
|
||||||
|
for (int i = 0; i < 50; ++i) {
|
||||||
|
btn_.press();
|
||||||
|
app.update();
|
||||||
|
btn_.release();
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(app.pressCount(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ButtonSystemTest, BouncyButtonWithSettling) {
|
||||||
|
ButtonSim bouncy_btn(5); // 5 reads of bounce
|
||||||
|
bouncy_btn.setSeed(42);
|
||||||
|
ButtonApp app(&sim_, &bouncy_btn);
|
||||||
|
app.begin();
|
||||||
|
|
||||||
|
bouncy_btn.press();
|
||||||
|
|
||||||
|
// During bounce, we may get spurious edges.
|
||||||
|
// After settling, one press should eventually register.
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one press should have registered
|
||||||
|
EXPECT_GE(app.pressCount(), 1);
|
||||||
|
}
|
||||||
31
templates/button/test/test_system.cpp.tmpl
Normal file
31
templates/button/test/test_system.cpp.tmpl
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* test_system.cpp -- Your system tests go here.
|
||||||
|
*
|
||||||
|
* This file is YOURS. Anvil will never overwrite it.
|
||||||
|
* The button example tests are in test_button_app.cpp.
|
||||||
|
*
|
||||||
|
* System tests use SimHal and ButtonSim to exercise real application
|
||||||
|
* logic against simulated hardware. See test_button_app.cpp for examples.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "mock_arduino.h"
|
||||||
|
#include "hal.h"
|
||||||
|
#include "sim_hal.h"
|
||||||
|
#include "button_sim.h"
|
||||||
|
#include "{{PROJECT_NAME}}_app.h"
|
||||||
|
|
||||||
|
// Example: add your own system tests below
|
||||||
|
// TEST(MySystemTests, DescribeWhatItTests) {
|
||||||
|
// mock_arduino_reset();
|
||||||
|
// SimHal sim;
|
||||||
|
// ButtonSim btn(0); // 0 = no bounce
|
||||||
|
//
|
||||||
|
// ButtonApp app(&sim, &btn);
|
||||||
|
// app.begin();
|
||||||
|
//
|
||||||
|
// btn.press();
|
||||||
|
// app.update();
|
||||||
|
// EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
// }
|
||||||
34
templates/button/test/test_unit.cpp.tmpl
Normal file
34
templates/button/test/test_unit.cpp.tmpl
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* test_unit.cpp -- Your unit tests go here.
|
||||||
|
*
|
||||||
|
* This file is YOURS. Anvil will never overwrite it.
|
||||||
|
* The button example tests are in test_button_app.cpp.
|
||||||
|
*
|
||||||
|
* Unit tests use MockHal and ButtonMock to verify exact behavior
|
||||||
|
* without real hardware. See test_button_app.cpp for examples.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
|
||||||
|
#include "hal.h"
|
||||||
|
#include "mock_hal.h"
|
||||||
|
#include "button_mock.h"
|
||||||
|
#include "{{PROJECT_NAME}}_app.h"
|
||||||
|
|
||||||
|
using ::testing::_;
|
||||||
|
using ::testing::AnyNumber;
|
||||||
|
using ::testing::Return;
|
||||||
|
|
||||||
|
// Example: add your own tests below
|
||||||
|
// TEST(MyTests, DescribeWhatItTests) {
|
||||||
|
// ::testing::NiceMock<MockHal> mock;
|
||||||
|
// ButtonMock btn;
|
||||||
|
//
|
||||||
|
// ButtonApp app(&mock, &btn);
|
||||||
|
// app.begin();
|
||||||
|
//
|
||||||
|
// btn.setPressed(true);
|
||||||
|
// app.update();
|
||||||
|
// EXPECT_EQ(app.pressCount(), 1);
|
||||||
|
// }
|
||||||
400
tests/test_template_button.rs
Normal file
400
tests/test_template_button.rs
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
use anvil::commands;
|
||||||
|
use anvil::library;
|
||||||
|
use anvil::project::config::ProjectConfig;
|
||||||
|
use anvil::templates::{TemplateManager, TemplateContext};
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Template listing and metadata
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_list_templates_includes_button() {
|
||||||
|
let templates = TemplateManager::list_templates();
|
||||||
|
assert!(
|
||||||
|
templates.iter().any(|t| t.name == "button"),
|
||||||
|
"Should list button template"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_template_exists() {
|
||||||
|
assert!(TemplateManager::template_exists("button"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_is_not_default() {
|
||||||
|
let templates = TemplateManager::list_templates();
|
||||||
|
let btn = templates.iter().find(|t| t.name == "button").unwrap();
|
||||||
|
assert!(!btn.is_default);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_lists_button_library() {
|
||||||
|
let templates = TemplateManager::list_templates();
|
||||||
|
let btn = templates.iter().find(|t| t.name == "button").unwrap();
|
||||||
|
assert!(btn.libraries.contains(&"button".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_lists_digital_capability() {
|
||||||
|
let templates = TemplateManager::list_templates();
|
||||||
|
let btn = templates.iter().find(|t| t.name == "button").unwrap();
|
||||||
|
assert!(btn.board_capabilities.contains(&"digital".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Composed metadata
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_composed_meta_exists() {
|
||||||
|
let meta = TemplateManager::composed_meta("button");
|
||||||
|
assert!(meta.is_some(), "Button should have composed metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_composed_meta_base_is_basic() {
|
||||||
|
let meta = TemplateManager::composed_meta("button").unwrap();
|
||||||
|
assert_eq!(meta.base, "basic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_composed_meta_requires_button_lib() {
|
||||||
|
let meta = TemplateManager::composed_meta("button").unwrap();
|
||||||
|
assert!(meta.libraries.contains(&"button".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_composed_meta_has_pin_defaults() {
|
||||||
|
let meta = TemplateManager::composed_meta("button").unwrap();
|
||||||
|
assert!(
|
||||||
|
!meta.pins.is_empty(),
|
||||||
|
"Should have pin defaults"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_pins_for_uno() {
|
||||||
|
let meta = TemplateManager::composed_meta("button").unwrap();
|
||||||
|
let pins = meta.pins_for_board("uno");
|
||||||
|
assert_eq!(pins.len(), 1);
|
||||||
|
assert_eq!(pins[0].name, "button_signal");
|
||||||
|
assert_eq!(pins[0].pin, "2");
|
||||||
|
assert_eq!(pins[0].mode, "input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_pins_fallback_to_default() {
|
||||||
|
let meta = TemplateManager::composed_meta("button").unwrap();
|
||||||
|
// "micro" is not explicitly listed, should fall back to "default"
|
||||||
|
let pins = meta.pins_for_board("micro");
|
||||||
|
assert!(!pins.is_empty());
|
||||||
|
assert!(pins.iter().any(|p| p.name == "button_signal"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Template extraction
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
fn extract_button(name: &str) -> TempDir {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let ctx = TemplateContext {
|
||||||
|
project_name: name.to_string(),
|
||||||
|
anvil_version: "1.0.0".to_string(),
|
||||||
|
board_name: "uno".to_string(),
|
||||||
|
fqbn: "arduino:avr:uno".to_string(),
|
||||||
|
baud: 115200,
|
||||||
|
};
|
||||||
|
TemplateManager::extract("button", tmp.path(), &ctx).unwrap();
|
||||||
|
// Record template name in config, same as create_project does
|
||||||
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
config.project.template = "button".to_string();
|
||||||
|
config.save(tmp.path()).unwrap();
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_basic_scaffold() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
// Basic scaffold from base template
|
||||||
|
assert!(tmp.path().join("lib/hal/hal.h").exists());
|
||||||
|
assert!(tmp.path().join("lib/hal/hal_arduino.h").exists());
|
||||||
|
assert!(tmp.path().join("test/mocks/mock_hal.h").exists());
|
||||||
|
assert!(tmp.path().join("test/mocks/sim_hal.h").exists());
|
||||||
|
assert!(tmp.path().join("test/CMakeLists.txt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_button_app() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let app = tmp.path().join("lib/app/btn_app.h");
|
||||||
|
assert!(app.exists(), "Should have button app header");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&app).unwrap();
|
||||||
|
assert!(content.contains("class ButtonApp"), "Should define ButtonApp");
|
||||||
|
assert!(content.contains("isPressed"), "Should use button interface");
|
||||||
|
assert!(content.contains("was_pressed_"), "Should track previous state");
|
||||||
|
assert!(content.contains("press_count_"), "Should count presses");
|
||||||
|
assert!(content.contains("serialPrintln"), "Should print to serial");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_app_replaces_basic_blink() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let app = tmp.path().join("lib/app/btn_app.h");
|
||||||
|
let content = fs::read_to_string(&app).unwrap();
|
||||||
|
|
||||||
|
// Should NOT contain basic template's BlinkApp
|
||||||
|
assert!(!content.contains("BlinkApp"), "Should not have BlinkApp");
|
||||||
|
assert!(content.contains("ButtonApp"), "Should have ButtonApp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_button_sketch() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let sketch = tmp.path().join("btn/btn.ino");
|
||||||
|
assert!(sketch.exists(), "Sketch should exist");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&sketch).unwrap();
|
||||||
|
assert!(content.contains("button_digital.h"), "Should include button driver");
|
||||||
|
assert!(content.contains("ButtonDigital"), "Should create ButtonDigital");
|
||||||
|
assert!(content.contains("ButtonApp"), "Should create ButtonApp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_sketch_replaces_basic_sketch() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let sketch = tmp.path().join("btn/btn.ino");
|
||||||
|
let content = fs::read_to_string(&sketch).unwrap();
|
||||||
|
|
||||||
|
assert!(!content.contains("BlinkApp"), "Should not reference BlinkApp");
|
||||||
|
assert!(content.contains("ButtonApp"), "Should reference ButtonApp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_managed_example_tests() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let test_file = tmp.path().join("test/test_button_app.cpp");
|
||||||
|
assert!(test_file.exists(), "Should have managed test file");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&test_file).unwrap();
|
||||||
|
assert!(content.contains("MANAGED BY ANVIL"), "Should be marked as managed");
|
||||||
|
assert!(content.contains("ButtonUnitTest"), "Should have unit test fixture");
|
||||||
|
assert!(content.contains("ButtonSystemTest"), "Should have system test fixture");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_student_unit_starter() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let test_file = tmp.path().join("test/test_unit.cpp");
|
||||||
|
assert!(test_file.exists());
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&test_file).unwrap();
|
||||||
|
assert!(content.contains("YOURS"), "Should be marked as student-owned");
|
||||||
|
assert!(content.contains("button_mock.h"), "Should include button mock");
|
||||||
|
assert!(content.contains("btn_app.h"), "Should include project app");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_has_student_system_starter() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let test_file = tmp.path().join("test/test_system.cpp");
|
||||||
|
assert!(test_file.exists());
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&test_file).unwrap();
|
||||||
|
assert!(content.contains("YOURS"), "Should be marked as student-owned");
|
||||||
|
assert!(content.contains("button_sim.h"), "Should include button sim");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_no_template_toml_in_output() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
assert!(
|
||||||
|
!tmp.path().join("template.toml").exists(),
|
||||||
|
"template.toml should not be extracted to output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_preserves_cmake() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
let cmake = tmp.path().join("test/CMakeLists.txt");
|
||||||
|
assert!(cmake.exists(), "CMakeLists.txt should survive overlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_variable_substitution() {
|
||||||
|
let tmp = extract_button("mybutton");
|
||||||
|
|
||||||
|
// Sketch should use project name
|
||||||
|
let sketch = tmp.path().join("mybutton/mybutton.ino");
|
||||||
|
let content = fs::read_to_string(&sketch).unwrap();
|
||||||
|
assert!(content.contains("mybutton_app.h"), "Should substitute project name");
|
||||||
|
assert!(!content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders");
|
||||||
|
|
||||||
|
// App header should use project name
|
||||||
|
let app = tmp.path().join("lib/app/mybutton_app.h");
|
||||||
|
assert!(app.exists(), "App header should use project name");
|
||||||
|
|
||||||
|
// Test files should use project name
|
||||||
|
let test_file = tmp.path().join("test/test_button_app.cpp");
|
||||||
|
let test_content = fs::read_to_string(&test_file).unwrap();
|
||||||
|
assert!(test_content.contains("mybutton_app.h"), "Test should include project app");
|
||||||
|
assert!(!test_content.contains("{{PROJECT_NAME}}"), "No unresolved placeholders");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_all_files_ascii() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
for entry in walkdir(tmp.path()) {
|
||||||
|
let content = fs::read_to_string(&entry).unwrap_or_default();
|
||||||
|
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}",
|
||||||
|
entry.display(),
|
||||||
|
line_num + 1, col + 1, ch as u32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// C++ API compatibility: test file references valid sensor methods
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_template_button_tests_use_valid_button_api() {
|
||||||
|
let tmp = extract_button("btn");
|
||||||
|
|
||||||
|
// Extract the button library so we can check method names
|
||||||
|
library::extract_library("button", tmp.path()).unwrap();
|
||||||
|
|
||||||
|
// Read mock and sim headers to build the set of valid methods
|
||||||
|
let mock_h = fs::read_to_string(
|
||||||
|
tmp.path().join("lib/drivers/button/button_mock.h")
|
||||||
|
).unwrap();
|
||||||
|
let sim_h = fs::read_to_string(
|
||||||
|
tmp.path().join("lib/drivers/button/button_sim.h")
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let test_file = fs::read_to_string(
|
||||||
|
tmp.path().join("test/test_button_app.cpp")
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Check that test calls methods that exist in mock or sim
|
||||||
|
let expected_methods = ["isPressed", "setPressed", "press", "release", "setSeed", "setBounceReads"];
|
||||||
|
for method in &expected_methods {
|
||||||
|
let in_mock = mock_h.contains(method);
|
||||||
|
let in_sim = sim_h.contains(method);
|
||||||
|
let in_test = test_file.contains(method);
|
||||||
|
if in_test {
|
||||||
|
assert!(
|
||||||
|
in_mock || in_sim,
|
||||||
|
"Test uses {}() but it is not in mock or sim headers",
|
||||||
|
method
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Full flow: new project with button template
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_full_flow() {
|
||||||
|
let tmp = extract_button("btn_flow");
|
||||||
|
|
||||||
|
// Install the button library (as anvil new --template button would)
|
||||||
|
let meta = library::find_library("button").unwrap();
|
||||||
|
library::extract_library("button", tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
config.libraries.insert("button".to_string(), meta.version.clone());
|
||||||
|
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||||
|
if !config.build.include_dirs.contains(&driver_include) {
|
||||||
|
config.build.include_dirs.push(driver_include);
|
||||||
|
}
|
||||||
|
config.save(tmp.path()).unwrap();
|
||||||
|
|
||||||
|
// Assign pin
|
||||||
|
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||||
|
commands::pin::assign_pin(
|
||||||
|
"button_signal", "2",
|
||||||
|
Some("input"),
|
||||||
|
None,
|
||||||
|
Some(&dir_str),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Verify everything is in place
|
||||||
|
let config_after = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
assert!(config_after.libraries.contains_key("button"));
|
||||||
|
let board_pins = config_after.pins.get("uno").unwrap();
|
||||||
|
assert!(board_pins.assignments.contains_key("button_signal"));
|
||||||
|
|
||||||
|
// Files exist
|
||||||
|
assert!(tmp.path().join("lib/drivers/button/button.h").exists());
|
||||||
|
assert!(tmp.path().join("lib/drivers/button/button_digital.h").exists());
|
||||||
|
assert!(tmp.path().join("lib/drivers/button/button_mock.h").exists());
|
||||||
|
assert!(tmp.path().join("lib/drivers/button/button_sim.h").exists());
|
||||||
|
assert!(tmp.path().join("test/test_button.cpp").exists());
|
||||||
|
assert!(tmp.path().join("test/test_button_app.cpp").exists());
|
||||||
|
assert!(tmp.path().join("lib/app/btn_flow_app.h").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_config_records_template_name() {
|
||||||
|
let tmp = extract_button("tmpl_name");
|
||||||
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
assert_eq!(config.project.template.as_str(), "button");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_button_audit_clean_after_full_setup() {
|
||||||
|
let tmp = extract_button("audit_btn");
|
||||||
|
|
||||||
|
let meta = library::find_library("button").unwrap();
|
||||||
|
library::extract_library("button", tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
config.libraries.insert("button".to_string(), meta.version.clone());
|
||||||
|
config.save(tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||||
|
commands::pin::assign_pin(
|
||||||
|
"button_signal", "2",
|
||||||
|
Some("input"),
|
||||||
|
None,
|
||||||
|
Some(&dir_str),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Audit should pass cleanly
|
||||||
|
commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// Helper: walk all files in a directory tree
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
fn walkdir(root: &std::path::Path) -> Vec<std::path::PathBuf> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
walk(&path, out);
|
||||||
|
} else if path.is_file() {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(root, &mut files);
|
||||||
|
files
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user