From e12608370acbc8fffe8fab1b2386a23bca80aea7 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sun, 22 Feb 2026 17:06:02 -0600 Subject: [PATCH] New button template --- README.md | 11 +- src/templates/mod.rs | 7 +- templates/button/__name__/__name__.ino.tmpl | 35 ++ templates/button/lib/app/__name___app.h.tmpl | 77 ++++ templates/button/template.toml | 25 ++ .../button/test/test_button_app.cpp.tmpl | 283 +++++++++++++ templates/button/test/test_system.cpp.tmpl | 31 ++ templates/button/test/test_unit.cpp.tmpl | 34 ++ tests/test_template_button.rs | 400 ++++++++++++++++++ 9 files changed, 898 insertions(+), 5 deletions(-) create mode 100644 templates/button/__name__/__name__.ino.tmpl create mode 100644 templates/button/lib/app/__name___app.h.tmpl create mode 100644 templates/button/template.toml create mode 100644 templates/button/test/test_button_app.cpp.tmpl create mode 100644 templates/button/test/test_system.cpp.tmpl create mode 100644 templates/button/test/test_unit.cpp.tmpl create mode 100644 tests/test_template_button.rs diff --git a/README.md b/README.md index 1fb8bde..24fc629 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,14 @@ point, use a composed template: ```bash 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 -temperature sensor driver, managed example tests demonstrating both mock and -simulator patterns, and student test starters. To see available templates: +The **weather** template adds a `WeatherApp` with a TMP36 temperature sensor, +managed example tests, and student test starters. The **button** template adds +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 anvil new --list-templates @@ -437,7 +440,7 @@ Upload these to a Gitea release. The script requires `build-essential`, cargo test ``` -615 tests (137 unit + 478 integration), ~4 seconds, zero warnings. +642 tests (137 unit + 505 integration), ~4 seconds, zero warnings. --- diff --git a/src/templates/mod.rs b/src/templates/mod.rs index de8aa48..7f0364b 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -10,6 +10,7 @@ use crate::version::ANVIL_VERSION; // Embedded template directories static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic"); 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. pub struct TemplateContext { @@ -106,13 +107,17 @@ fn template_dir(name: &str) -> Option<&'static Dir<'static>> { match name { "basic" => Some(&BASIC_TEMPLATE), "weather" => Some(&WEATHER_TEMPLATE), + "button" => Some(&BUTTON_TEMPLATE), _ => None, } } // All composed templates (everything except "basic"). fn composed_template_entries() -> Vec<(&'static str, &'static Dir<'static>)> { - vec![("weather", &WEATHER_TEMPLATE)] + vec![ + ("weather", &WEATHER_TEMPLATE), + ("button", &BUTTON_TEMPLATE), + ] } // ========================================================================= diff --git a/templates/button/__name__/__name__.ino.tmpl b/templates/button/__name__/__name__.ino.tmpl new file mode 100644 index 0000000..64acee3 --- /dev/null +++ b/templates/button/__name__/__name__.ino.tmpl @@ -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 +#include <{{PROJECT_NAME}}_app.h> +#include + +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(); +} diff --git a/templates/button/lib/app/__name___app.h.tmpl b/templates/button/lib/app/__name___app.h.tmpl new file mode 100644 index 0000000..b7f4d58 --- /dev/null +++ b/templates/button/lib/app/__name___app.h.tmpl @@ -0,0 +1,77 @@ +#ifndef APP_H +#define APP_H + +#include +#include "button.h" + +#include + +/* + * 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 diff --git a/templates/button/template.toml b/templates/button/template.toml new file mode 100644 index 0000000..e9fde0a --- /dev/null +++ b/templates/button/template.toml @@ -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" } diff --git a/templates/button/test/test_button_app.cpp.tmpl b/templates/button/test/test_button_app.cpp.tmpl new file mode 100644 index 0000000..80b6b05 --- /dev/null +++ b/templates/button/test/test_button_app.cpp.tmpl @@ -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 +#include + +#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 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); +} diff --git a/templates/button/test/test_system.cpp.tmpl b/templates/button/test/test_system.cpp.tmpl new file mode 100644 index 0000000..bf04afa --- /dev/null +++ b/templates/button/test/test_system.cpp.tmpl @@ -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 + +#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); +// } diff --git a/templates/button/test/test_unit.cpp.tmpl b/templates/button/test/test_unit.cpp.tmpl new file mode 100644 index 0000000..5d78026 --- /dev/null +++ b/templates/button/test/test_unit.cpp.tmpl @@ -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 +#include + +#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 mock; +// ButtonMock btn; +// +// ButtonApp app(&mock, &btn); +// app.begin(); +// +// btn.setPressed(true); +// app.update(); +// EXPECT_EQ(app.pressCount(), 1); +// } diff --git a/tests/test_template_button.rs b/tests/test_template_button.rs new file mode 100644 index 0000000..c0b1108 --- /dev/null +++ b/tests/test_template_button.rs @@ -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 { + let mut files = Vec::new(); + fn walk(dir: &std::path::Path, out: &mut Vec) { + 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 +} \ No newline at end of file