Supporting a button library.
This commit is contained in:
15
libraries/button/library.toml
Normal file
15
libraries/button/library.toml
Normal file
@@ -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"
|
||||
52
libraries/button/src/button.h
Normal file
52
libraries/button/src/button.h
Normal file
@@ -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
|
||||
57
libraries/button/src/button_digital.h
Normal file
57
libraries/button/src/button_digital.h
Normal file
@@ -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
|
||||
60
libraries/button/src/button_mock.h
Normal file
60
libraries/button/src/button_mock.h
Normal file
@@ -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
|
||||
100
libraries/button/src/button_sim.h
Normal file
100
libraries/button/src/button_sim.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#ifndef BUTTON_SIM_H
|
||||
#define BUTTON_SIM_H
|
||||
|
||||
#include "button.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
/*
|
||||
* 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
|
||||
289
libraries/button/src/test_button.cpp
Normal file
289
libraries/button/src/test_button.cpp
Normal file
@@ -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 <gtest/gtest.h>
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -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::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[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<String> = 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());
|
||||
}
|
||||
}
|
||||
@@ -616,3 +616,213 @@ fn test_add_remove_pin_assignment_survives() {
|
||||
"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());
|
||||
}
|
||||
Reference in New Issue
Block a user