From 970479f4b6d54263ffec7fd9fbd1edd504c204ca Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sun, 22 Feb 2026 07:44:41 -0600 Subject: [PATCH] Supporting a button library. --- libraries/button/library.toml | 15 ++ libraries/button/src/button.h | 52 +++++ libraries/button/src/button_digital.h | 57 +++++ libraries/button/src/button_mock.h | 60 ++++++ libraries/button/src/button_sim.h | 100 +++++++++ libraries/button/src/test_button.cpp | 289 ++++++++++++++++++++++++++ src/library/mod.rs | 139 +++++++++++++ tests/test_library.rs | 210 +++++++++++++++++++ 8 files changed, 922 insertions(+) create mode 100644 libraries/button/library.toml create mode 100644 libraries/button/src/button.h create mode 100644 libraries/button/src/button_digital.h create mode 100644 libraries/button/src/button_mock.h create mode 100644 libraries/button/src/button_sim.h create mode 100644 libraries/button/src/test_button.cpp diff --git a/libraries/button/library.toml b/libraries/button/library.toml new file mode 100644 index 0000000..6195d04 --- /dev/null +++ b/libraries/button/library.toml @@ -0,0 +1,15 @@ +[library] +name = "button" +version = "0.1.0" +description = "Momentary pushbutton with debounce support" + +[requires] +bus = "digital" +pins = ["signal"] + +[provides] +interface = "button.h" +implementation = "button_digital.h" +mock = "button_mock.h" +simulation = "button_sim.h" +test = "test_button.cpp" diff --git a/libraries/button/src/button.h b/libraries/button/src/button.h new file mode 100644 index 0000000..55b946a --- /dev/null +++ b/libraries/button/src/button.h @@ -0,0 +1,52 @@ +#ifndef BUTTON_H +#define BUTTON_H + +/* + * Momentary Pushbutton -- Abstract Interface + * + * This is the contract that your application code depends on. It does + * not know or care whether the button state comes from a real GPIO + * pin, a test mock with canned presses, or a simulation with + * realistic contact bounce. + * + * Three implementations ship with this driver: + * button_digital.h -- Real hardware via Hal::digitalRead() + * button_mock.h -- Test mock with programmable state + * button_sim.h -- Simulation with configurable bounce + * + * Usage in application code: + * #include "button.h" + * + * class ToggleApp { + * public: + * ToggleApp(Hal* hal, Button* btn) + * : hal_(hal), btn_(btn) {} + * + * void update() { + * if (btn_->isPressed()) { + * hal_->digitalWrite(LED_PIN, HIGH); + * } else { + * hal_->digitalWrite(LED_PIN, LOW); + * } + * } + * }; + * + * Generated by Anvil -- https://nxgit.dev/nexus-workshops/anvil + */ + +class Button { +public: + virtual ~Button() = default; + + /// Is the button currently pressed? + /// Returns true when pressed, false when released. + /// Implementations handle active-low vs active-high internally. + virtual bool isPressed() = 0; + + /// Read the raw digital state (HIGH or LOW). + /// No inversion -- returns exactly what the pin reads. + /// Useful for debugging or custom logic. + virtual int readState() = 0; +}; + +#endif // BUTTON_H diff --git a/libraries/button/src/button_digital.h b/libraries/button/src/button_digital.h new file mode 100644 index 0000000..4c511da --- /dev/null +++ b/libraries/button/src/button_digital.h @@ -0,0 +1,57 @@ +#ifndef BUTTON_DIGITAL_H +#define BUTTON_DIGITAL_H + +#include "button.h" +#include "hal.h" + +/* + * Button -- Real hardware implementation (digital read). + * + * Reads a physical pushbutton connected to a digital pin. + * Supports both wiring configurations: + * + * Active-low (most common, use INPUT_PULLUP): + * Pin -> Button -> GND + * Reads LOW when pressed, HIGH when released. + * Set active_low = true (default). + * + * Active-high (external pull-down resistor): + * Pin -> Button -> VCC, with pull-down to GND + * Reads HIGH when pressed, LOW when released. + * Set active_low = false. + * + * Wiring (active-low, no external resistor needed): + * Digital pin -> one leg of button + * GND -> other leg of button + * Set pin mode to INPUT_PULLUP in your sketch. + * + * Note: This implementation does NOT debounce. If your application + * needs debounce (most do), implement it in your app logic -- that + * way you can test it with the mock and simulator. + */ +class ButtonDigital : public Button { +public: + /// Create a button on the given digital pin. + /// active_low: true if pressed reads LOW (default, use with INPUT_PULLUP). + ButtonDigital(Hal* hal, uint8_t pin, bool active_low = true) + : hal_(hal) + , pin_(pin) + , active_low_(active_low) + {} + + bool isPressed() override { + int state = hal_->digitalRead(pin_); + return active_low_ ? (state == LOW) : (state == HIGH); + } + + int readState() override { + return hal_->digitalRead(pin_); + } + +private: + Hal* hal_; + uint8_t pin_; + bool active_low_; +}; + +#endif // BUTTON_DIGITAL_H diff --git a/libraries/button/src/button_mock.h b/libraries/button/src/button_mock.h new file mode 100644 index 0000000..45cfbbb --- /dev/null +++ b/libraries/button/src/button_mock.h @@ -0,0 +1,60 @@ +#ifndef BUTTON_MOCK_H +#define BUTTON_MOCK_H + +#include "button.h" + +/* + * Button -- Test mock with programmable state. + * + * Use this in tests to control exactly what button state your + * application sees, without any real hardware. + * + * Example: + * ButtonMock btn; + * btn.setPressed(true); + * + * ToggleApp app(&sim, &btn); + * app.update(); + * + * EXPECT_EQ(sim.getPin(LED_PIN), HIGH); // LED should be on + * + * You can also track how many times the button was read: + * btn.resetReadCount(); + * app.update(); + * EXPECT_EQ(btn.readCount(), 1); + */ +class ButtonMock : public Button { +public: + /// Set whether the button appears pressed. + void setPressed(bool pressed) { + pressed_ = pressed; + } + + /// Set the raw digital state returned by readState(). + void setRaw(int value) { + raw_ = value; + } + + bool isPressed() override { + ++read_count_; + return pressed_; + } + + int readState() override { + ++read_count_; + return raw_; + } + + /// How many times has the button been read? + int readCount() const { return read_count_; } + + /// Reset the read counter. + void resetReadCount() { read_count_ = 0; } + +private: + bool pressed_ = false; // Default: not pressed + int raw_ = HIGH; // Default: HIGH (active-low, not pressed) + int read_count_ = 0; +}; + +#endif // BUTTON_MOCK_H diff --git a/libraries/button/src/button_sim.h b/libraries/button/src/button_sim.h new file mode 100644 index 0000000..6122f37 --- /dev/null +++ b/libraries/button/src/button_sim.h @@ -0,0 +1,100 @@ +#ifndef BUTTON_SIM_H +#define BUTTON_SIM_H + +#include "button.h" + +#include + +/* + * Button -- Simulation with realistic contact bounce. + * + * Unlike the mock (which returns exact states), the simulator models + * contact bounce during press/release transitions. This is useful + * for system tests that verify your debounce logic, such as: + * + * - Software debounce algorithms (delay-based, counter-based) + * - Edge detection that should not double-trigger + * - State machine transitions that must be bounce-tolerant + * + * Example: + * ButtonSim btn; + * btn.setBounceReads(5); // 5 noisy reads per transition + * + * btn.press(); // start a press + * // First few reads may bounce between pressed/released + * // After bounce_reads, settles to pressed + * + * btn.release(); // start a release + * // Same bounce behavior during release + * + * For tests that do not care about bounce: + * ButtonSim btn; + * btn.setBounceReads(0); // no bounce, instant transitions + * btn.press(); + * EXPECT_TRUE(btn.isPressed()); // always true immediately + */ +class ButtonSim : public Button { +public: + /// Create a simulated button. + /// bounce_reads: how many noisy reads occur after each transition + ButtonSim(int bounce_reads = 0) + : pressed_(false) + , bounce_reads_(bounce_reads) + , reads_since_change_(0) + , seed_(42) + {} + + /// Press the button (start a press transition). + void press() { + if (!pressed_) { + pressed_ = true; + reads_since_change_ = 0; + } + } + + /// Release the button (start a release transition). + void release() { + if (pressed_) { + pressed_ = false; + reads_since_change_ = 0; + } + } + + /// Set the number of bouncy reads after each transition. + void setBounceReads(int count) { bounce_reads_ = count; } + + /// Seed the random number generator for repeatable bounce patterns. + void setSeed(unsigned int seed) { seed_ = seed; } + + bool isPressed() override { + if (reads_since_change_ < bounce_reads_) { + ++reads_since_change_; + // During bounce, randomly return wrong state + bool bounce_flip = (nextRandom() > 0.5f); + return bounce_flip ? !pressed_ : pressed_; + } + reads_since_change_ = bounce_reads_; // clamp + return pressed_; + } + + int readState() override { + // Map pressed state to pin level (active-low convention) + bool p = isPressed(); + return p ? LOW : HIGH; + } + +private: + bool pressed_; + int bounce_reads_; + int reads_since_change_; + unsigned int seed_; + + /// Simple LCG random in [0.0, 1.0). + /// Deterministic -- same seed gives same sequence. + float nextRandom() { + seed_ = seed_ * 1103515245 + 12345; + return (float)((seed_ >> 16) & 0x7FFF) / 32768.0f; + } +}; + +#endif // BUTTON_SIM_H diff --git a/libraries/button/src/test_button.cpp b/libraries/button/src/test_button.cpp new file mode 100644 index 0000000..3171bff --- /dev/null +++ b/libraries/button/src/test_button.cpp @@ -0,0 +1,289 @@ +/* + * Button Driver Tests + * + * Auto-generated by: anvil add button + * These tests verify the Button driver mock and simulation without + * any hardware. They run alongside your unit and system tests. + * + * To run: ./test.sh (all tests) + * ./test.sh --system (skips these -- use no filter to include) + * ./test.sh --unit (skips these -- use no filter to include) + */ + +#include +#include "mock_arduino.h" +#include "sim_hal.h" +#include "button.h" +#include "button_digital.h" +#include "button_mock.h" +#include "button_sim.h" + +// --------------------------------------------------------------------------- +// Mock: basic functionality +// --------------------------------------------------------------------------- + +class ButtonMockTest : public ::testing::Test { +protected: + void SetUp() override { + mock_arduino_reset(); + } + + ButtonMock btn; +}; + +TEST_F(ButtonMockTest, DefaultsToNotPressed) { + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonMockTest, SetPressedReturnsTrue) { + btn.setPressed(true); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonMockTest, SetReleasedReturnsFalse) { + btn.setPressed(true); + btn.setPressed(false); + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonMockTest, DefaultRawIsHigh) { + EXPECT_EQ(btn.readState(), HIGH); +} + +TEST_F(ButtonMockTest, SetRawReturnsExactValue) { + btn.setRaw(LOW); + EXPECT_EQ(btn.readState(), LOW); +} + +TEST_F(ButtonMockTest, ReadCountTracking) { + EXPECT_EQ(btn.readCount(), 0); + btn.isPressed(); + btn.isPressed(); + btn.readState(); + EXPECT_EQ(btn.readCount(), 3); +} + +TEST_F(ButtonMockTest, ReadCountReset) { + btn.isPressed(); + btn.isPressed(); + btn.resetReadCount(); + EXPECT_EQ(btn.readCount(), 0); +} + +TEST_F(ButtonMockTest, PressAndRawAreIndependent) { + // setPressed controls isPressed(), setRaw controls readState() + btn.setPressed(true); + btn.setRaw(HIGH); + EXPECT_TRUE(btn.isPressed()); + EXPECT_EQ(btn.readState(), HIGH); +} + +// --------------------------------------------------------------------------- +// Digital: real implementation via SimHal +// --------------------------------------------------------------------------- + +class ButtonDigitalTest : public ::testing::Test { +protected: + void SetUp() override { + mock_arduino_reset(); + } + + SimHal hal; +}; + +TEST_F(ButtonDigitalTest, ActiveLowPressedWhenLow) { + ButtonDigital btn(&hal, 2, true); // active-low + hal.setPin(2, LOW); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonDigitalTest, ActiveLowReleasedWhenHigh) { + ButtonDigital btn(&hal, 2, true); // active-low + hal.setPin(2, HIGH); + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonDigitalTest, ActiveHighPressedWhenHigh) { + ButtonDigital btn(&hal, 2, false); // active-high + hal.setPin(2, HIGH); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonDigitalTest, ActiveHighReleasedWhenLow) { + ButtonDigital btn(&hal, 2, false); // active-high + hal.setPin(2, LOW); + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonDigitalTest, ReadStateReturnsRawPin) { + ButtonDigital btn(&hal, 2, true); + hal.setPin(2, LOW); + EXPECT_EQ(btn.readState(), LOW); + + hal.setPin(2, HIGH); + EXPECT_EQ(btn.readState(), HIGH); +} + +TEST_F(ButtonDigitalTest, DifferentPin) { + ButtonDigital btn(&hal, 7, true); + hal.setPin(7, LOW); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonDigitalTest, DefaultIsActiveLow) { + ButtonDigital btn(&hal, 2); // active_low defaults to true + hal.setPin(2, LOW); + EXPECT_TRUE(btn.isPressed()); +} + +// --------------------------------------------------------------------------- +// Simulation: bounce and determinism +// --------------------------------------------------------------------------- + +class ButtonSimTest : public ::testing::Test { +protected: + void SetUp() override { + mock_arduino_reset(); + } +}; + +TEST_F(ButtonSimTest, DefaultsToNotPressed) { + ButtonSim btn; + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, PressWithNoBounceIsImmediate) { + ButtonSim btn(0); // no bounce + btn.press(); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, ReleaseWithNoBounceIsImmediate) { + ButtonSim btn(0); + btn.press(); + btn.release(); + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, BounceSettlesAfterEnoughReads) { + ButtonSim btn(5); // 5 bouncy reads + btn.press(); + + // Read through the bounce window + for (int i = 0; i < 5; ++i) { + btn.isPressed(); // may or may not bounce + } + + // After bounce window, should be stable + EXPECT_TRUE(btn.isPressed()); + EXPECT_TRUE(btn.isPressed()); + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, ReleaseBouncesSettleToReleased) { + ButtonSim btn(3); + btn.press(); + // Clear press bounce + for (int i = 0; i < 10; ++i) btn.isPressed(); + + btn.release(); + // Read through release bounce + for (int i = 0; i < 3; ++i) btn.isPressed(); + + // Settled to released + EXPECT_FALSE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, DeterministicWithSameSeed) { + ButtonSim s1(5); + ButtonSim s2(5); + s1.setSeed(99); + s2.setSeed(99); + + s1.press(); + s2.press(); + + for (int i = 0; i < 10; ++i) { + EXPECT_EQ(s1.isPressed(), s2.isPressed()) + << "Reading " << i << " should match with same seed"; + } +} + +TEST_F(ButtonSimTest, DifferentSeedsDifferentBounce) { + ButtonSim s1(10); + ButtonSim s2(10); + s1.setSeed(1); + s2.setSeed(2); + + s1.press(); + s2.press(); + + // At least one reading during bounce should differ + bool any_differ = false; + for (int i = 0; i < 10; ++i) { + if (s1.isPressed() != s2.isPressed()) { + any_differ = true; + break; + } + } + EXPECT_TRUE(any_differ); +} + +TEST_F(ButtonSimTest, NoBounceZeroReads) { + ButtonSim btn(0); + btn.press(); + // With zero bounce, every read should be stable + for (int i = 0; i < 20; ++i) { + EXPECT_TRUE(btn.isPressed()) << "Read " << i << " should be pressed"; + } +} + +TEST_F(ButtonSimTest, ReadStateMatchesIsPressed) { + ButtonSim btn(0); + btn.press(); + // Active-low convention: pressed -> LOW + EXPECT_EQ(btn.readState(), LOW); + + btn.release(); + EXPECT_EQ(btn.readState(), HIGH); +} + +TEST_F(ButtonSimTest, DoublePressSameState) { + ButtonSim btn(0); + btn.press(); + btn.press(); // second press is a no-op + EXPECT_TRUE(btn.isPressed()); +} + +TEST_F(ButtonSimTest, DoubleReleaseSameState) { + ButtonSim btn(0); + btn.release(); // already released, no-op + EXPECT_FALSE(btn.isPressed()); +} + +// --------------------------------------------------------------------------- +// Polymorphism: all impls work through Button pointer +// --------------------------------------------------------------------------- + +TEST(ButtonPolymorphismTest, AllImplsWorkThroughBasePointer) { + mock_arduino_reset(); + SimHal hal; + hal.setDigital(2, LOW); // pressed for active-low + + ButtonDigital digital_btn(&hal, 2, true); + ButtonMock mock_btn; + ButtonSim sim_btn(0); + + mock_btn.setPressed(true); + sim_btn.press(); + + Button* buttons[] = { &digital_btn, &mock_btn, &sim_btn }; + + for (auto* b : buttons) { + bool pressed = b->isPressed(); + int raw = b->readState(); + + EXPECT_TRUE(pressed); + (void)raw; // just verify it compiles and runs + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index 7c5fc95..d124171 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -355,4 +355,143 @@ mod tests { let missing = unassigned_pins(&meta, &assigned); assert!(missing.is_empty()); } + + // ── Button library tests ──────────────────────────────────────────── + + #[test] + fn test_list_available_includes_button() { + let libs = list_available(); + assert!( + libs.iter().any(|l| l.name == "button"), + "Should include button library, found: {:?}", + libs.iter().map(|l| &l.name).collect::>() + ); + } + + #[test] + fn test_find_library_button() { + let meta = find_library("button").expect("button should exist"); + assert_eq!(meta.name, "button"); + assert_eq!(meta.bus, "digital"); + assert_eq!(meta.pins, vec!["signal"]); + assert_eq!(meta.interface, "button.h"); + assert_eq!(meta.implementation, "button_digital.h"); + assert_eq!(meta.mock, "button_mock.h"); + assert!(meta.simulation.is_some()); + assert_eq!(meta.simulation.as_deref(), Some("button_sim.h")); + assert_eq!(meta.test.as_deref(), Some("test_button.cpp")); + } + + #[test] + fn test_extract_button_creates_files() { + let tmp = TempDir::new().unwrap(); + let written = extract_library("button", tmp.path()).unwrap(); + + assert!(!written.is_empty(), "Should write at least one file"); + + let driver_dir = tmp.path().join("lib/drivers/button"); + assert!(driver_dir.exists(), "Driver directory should exist"); + assert!(driver_dir.join("button.h").exists(), "Interface should exist"); + assert!(driver_dir.join("button_digital.h").exists(), "Implementation should exist"); + assert!(driver_dir.join("button_mock.h").exists(), "Mock should exist"); + assert!(driver_dir.join("button_sim.h").exists(), "Simulation should exist"); + assert!( + tmp.path().join("test/test_button.cpp").exists(), + "Test file should be in test/ directory" + ); + } + + #[test] + fn test_extract_button_files_are_ascii() { + let tmp = TempDir::new().unwrap(); + extract_library("button", tmp.path()).unwrap(); + + let driver_dir = tmp.path().join("lib/drivers/button"); + for entry in fs::read_dir(&driver_dir).unwrap() { + let entry = entry.unwrap(); + let content = fs::read_to_string(entry.path()).unwrap(); + 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.file_name().to_string_lossy(), + line_num + 1, col + 1, ch as u32 + ); + } + } + } + } + + #[test] + fn test_remove_button_cleans_up() { + let tmp = TempDir::new().unwrap(); + extract_library("button", tmp.path()).unwrap(); + + assert!(is_installed_on_disk("button", tmp.path())); + assert!(tmp.path().join("test/test_button.cpp").exists()); + remove_library_files("button", tmp.path()).unwrap(); + assert!(!is_installed_on_disk("button", tmp.path())); + assert!(!tmp.path().join("test/test_button.cpp").exists()); + } + + #[test] + fn test_wiring_summary_digital() { + let meta = find_library("button").unwrap(); + let summary = meta.wiring_summary(); + assert!(summary.contains("digital"), "Should mention digital: {}", summary); + assert!(summary.contains("1"), "Should mention 1 pin: {}", summary); + } + + #[test] + fn test_default_mode_digital() { + let meta = find_library("button").unwrap(); + assert_eq!(meta.default_mode(), "input"); + } + + #[test] + fn test_button_pin_roles() { + let meta = find_library("button").unwrap(); + let roles = meta.pin_roles(); + assert_eq!(roles.len(), 1); + assert_eq!(roles[0].0, "signal"); + assert_eq!(roles[0].1, "button_signal"); + } + + #[test] + fn test_button_unassigned_pins() { + let meta = find_library("button").unwrap(); + let assigned: Vec = vec![]; + let missing = unassigned_pins(&meta, &assigned); + assert_eq!(missing, vec!["button_signal"]); + } + + #[test] + fn test_button_unassigned_pins_when_assigned() { + let meta = find_library("button").unwrap(); + let assigned = vec!["button_signal".to_string()]; + let missing = unassigned_pins(&meta, &assigned); + assert!(missing.is_empty()); + } + + #[test] + fn test_both_libraries_coexist() { + let tmp = TempDir::new().unwrap(); + extract_library("tmp36", tmp.path()).unwrap(); + extract_library("button", tmp.path()).unwrap(); + + // Both driver directories exist + assert!(tmp.path().join("lib/drivers/tmp36").is_dir()); + assert!(tmp.path().join("lib/drivers/button").is_dir()); + + // Both test files exist + assert!(tmp.path().join("test/test_tmp36.cpp").exists()); + assert!(tmp.path().join("test/test_button.cpp").exists()); + + // Remove one, the other survives + remove_library_files("tmp36", tmp.path()).unwrap(); + assert!(!is_installed_on_disk("tmp36", tmp.path())); + assert!(is_installed_on_disk("button", tmp.path())); + assert!(tmp.path().join("test/test_button.cpp").exists()); + } } \ No newline at end of file diff --git a/tests/test_library.rs b/tests/test_library.rs index 86fe4f7..facb6f9 100644 --- a/tests/test_library.rs +++ b/tests/test_library.rs @@ -615,4 +615,214 @@ fn test_add_remove_pin_assignment_survives() { board_pins.assignments.contains_key("tmp36_data"), "Pin assignment should survive library removal" ); +} + +// ========================================================================== +// Button Library: registry, extraction, content, coexistence +// ========================================================================== + +#[test] +fn test_library_registry_lists_button() { + let libs = library::list_available(); + let btn = libs.iter().find(|l| l.name == "button"); + assert!(btn.is_some(), "Button should be in the registry"); + + let meta = btn.unwrap(); + assert_eq!(meta.bus, "digital"); + assert_eq!(meta.pins, vec!["signal"]); + assert_eq!(meta.interface, "button.h"); + assert_eq!(meta.mock, "button_mock.h"); +} + +#[test] +fn test_button_extract_creates_driver_directory() { + let tmp = TempDir::new().unwrap(); + + let written = library::extract_library("button", tmp.path()).unwrap(); + assert!(!written.is_empty(), "Should write files"); + + let driver_dir = tmp.path().join("lib/drivers/button"); + assert!(driver_dir.exists(), "Driver directory should be created"); + + assert!(driver_dir.join("button.h").exists(), "Interface header"); + assert!(driver_dir.join("button_digital.h").exists(), "Implementation"); + assert!(driver_dir.join("button_mock.h").exists(), "Mock"); + assert!(driver_dir.join("button_sim.h").exists(), "Simulation"); +} + +#[test] +fn test_button_extract_files_content_is_valid() { + let tmp = TempDir::new().unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + let driver_dir = tmp.path().join("lib/drivers/button"); + + // Interface should define Button class + let interface = fs::read_to_string(driver_dir.join("button.h")).unwrap(); + assert!(interface.contains("class Button"), "Should define Button"); + assert!(interface.contains("isPressed"), "Should declare isPressed"); + assert!(interface.contains("readState"), "Should declare readState"); + + // Implementation should include hal.h + let impl_h = fs::read_to_string(driver_dir.join("button_digital.h")).unwrap(); + assert!(impl_h.contains("hal.h"), "Implementation should use HAL"); + assert!(impl_h.contains("class ButtonDigital"), "Should define ButtonDigital"); + assert!(impl_h.contains("digitalRead"), "Should use digitalRead"); + + // Mock should have setPressed + let mock_h = fs::read_to_string(driver_dir.join("button_mock.h")).unwrap(); + assert!(mock_h.contains("class ButtonMock"), "Should define ButtonMock"); + assert!(mock_h.contains("setPressed"), "Mock should have setPressed"); + + // Sim should have press/release and bounce + let sim_h = fs::read_to_string(driver_dir.join("button_sim.h")).unwrap(); + assert!(sim_h.contains("class ButtonSim"), "Should define ButtonSim"); + assert!(sim_h.contains("setBounceReads"), "Sim should have setBounceReads"); + assert!(sim_h.contains("press()"), "Sim should have press()"); + assert!(sim_h.contains("release()"), "Sim should have release()"); +} + +#[test] +fn test_button_files_are_ascii_only() { + let tmp = TempDir::new().unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + let driver_dir = tmp.path().join("lib/drivers/button"); + for entry in fs::read_dir(&driver_dir).unwrap() { + let entry = entry.unwrap(); + let content = fs::read_to_string(entry.path()).unwrap(); + 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.file_name().to_string_lossy(), + line_num + 1, col + 1, ch as u32 + ); + } + } + } +} + +#[test] +fn test_button_remove_cleans_up() { + let tmp = TempDir::new().unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + assert!(library::is_installed_on_disk("button", tmp.path())); + assert!(tmp.path().join("test/test_button.cpp").exists()); + + library::remove_library_files("button", tmp.path()).unwrap(); + + assert!(!library::is_installed_on_disk("button", tmp.path())); + assert!(!tmp.path().join("test/test_button.cpp").exists()); +} + +#[test] +fn test_button_meta_wiring_summary() { + let meta = library::find_library("button").unwrap(); + let summary = meta.wiring_summary(); + assert!(summary.contains("digital"), "Should mention digital bus: {}", summary); +} + +#[test] +fn test_button_meta_pin_roles() { + let meta = library::find_library("button").unwrap(); + let roles = meta.pin_roles(); + assert_eq!(roles.len(), 1); + assert_eq!(roles[0].0, "signal"); + assert_eq!(roles[0].1, "button_signal"); +} + +#[test] +fn test_button_meta_default_mode() { + let meta = library::find_library("button").unwrap(); + assert_eq!(meta.default_mode(), "input"); +} + +#[test] +fn test_button_interface_uses_polymorphism() { + let tmp = TempDir::new().unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + let driver_dir = tmp.path().join("lib/drivers/button"); + + // All implementations should inherit from Button + let impl_h = fs::read_to_string(driver_dir.join("button_digital.h")).unwrap(); + assert!(impl_h.contains(": public Button"), "ButtonDigital should inherit Button"); + + let mock_h = fs::read_to_string(driver_dir.join("button_mock.h")).unwrap(); + assert!(mock_h.contains(": public Button"), "ButtonMock should inherit Button"); + + let sim_h = fs::read_to_string(driver_dir.join("button_sim.h")).unwrap(); + assert!(sim_h.contains(": public Button"), "ButtonSim should inherit Button"); +} + +#[test] +fn test_add_button_full_flow() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "btn_flow".to_string(), + anvil_version: "1.0.0".to_string(), + board_name: "uno".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, + }; + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let meta = library::find_library("button").unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + let mut config = ProjectConfig::load(tmp.path()).unwrap(); + let driver_include = format!("lib/drivers/{}", meta.name); + config.build.include_dirs.push(driver_include); + config.libraries.insert(meta.name.clone(), meta.version.clone()); + config.save(tmp.path()).unwrap(); + + // Assign a digital 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(); + + 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")); + let assignment = &board_pins.assignments["button_signal"]; + assert_eq!(assignment.mode, "input"); +} + +#[test] +fn test_both_libraries_install_together() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "both_libs".to_string(), + anvil_version: "1.0.0".to_string(), + board_name: "uno".to_string(), + fqbn: "arduino:avr:uno".to_string(), + baud: 115200, + }; + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + library::extract_library("tmp36", tmp.path()).unwrap(); + library::extract_library("button", tmp.path()).unwrap(); + + // Both driver directories exist + assert!(tmp.path().join("lib/drivers/tmp36").is_dir()); + assert!(tmp.path().join("lib/drivers/button").is_dir()); + + // Both test files exist + assert!(tmp.path().join("test/test_tmp36.cpp").exists()); + assert!(tmp.path().join("test/test_button.cpp").exists()); + + // Remove button, tmp36 survives + library::remove_library_files("button", tmp.path()).unwrap(); + assert!(!library::is_installed_on_disk("button", tmp.path())); + assert!(library::is_installed_on_disk("tmp36", tmp.path())); + assert!(tmp.path().join("test/test_tmp36.cpp").exists()); + assert!(!tmp.path().join("test/test_button.cpp").exists()); } \ No newline at end of file