From e235db504dd6c1c83b28c1581d1e618198fc52da 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. --- files(3).zip | Bin 0 -> 8880 bytes 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 +++++++++++++++++++ 9 files changed, 922 insertions(+) create mode 100644 files(3).zip 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/files(3).zip b/files(3).zip new file mode 100644 index 0000000000000000000000000000000000000000..b5d81a785c86099ff79017dc0bc42a2dbc7ee753 GIT binary patch literal 8880 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 literal 0 HcmV?d00001 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