Skip to content

Commit 2a9257d

Browse files
committed
Add some unit tests
1 parent c7bd620 commit 2a9257d

File tree

4 files changed

+223
-57
lines changed

4 files changed

+223
-57
lines changed

Cargo.toml

+10-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
cfg-if = "1.0.0"
8+
79
embassy-time = { git = "https://github.com/embassy-rs/embassy/", version = "0.1.5" }
810
embedded-hal-async = { version = "1.0.0-rc.2" }
911
embedded-hal = { version = "1.0.0-rc.2" }
12+
1013
defmt = { version = "0.3.5", optional = true }
1114

1215
[dev-dependencies]
13-
16+
embedded-hal-mock = { path = "../embedded-hal-mock", default-features = false, features = [
17+
"eh1",
18+
"embedded-time",
19+
"embedded-hal-async",
20+
] }
21+
tokio = { version = "1.34.0", features = ["rt", "macros", "time", "test-util"] }
22+
claims = "0.7.1"
1423

1524
[features]
1625
default = []
1726
defmt = ["dep:defmt", "embassy-time/defmt"]
18-
std = []

rust-toolchain.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[toolchain]
2-
channel = "nightly"
2+
channel = "nightly" # Only needed for unstable rustfmt features

src/lib.rs

+86-54
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
#![doc = include_str!("../README.md")]
2-
#![cfg_attr(not(feature = "std"), no_std)]
2+
#![cfg_attr(not(any(test, feature = "std")), no_std)]
33
#![warn(missing_docs)]
44

55
pub use config::{ButtonConfig, Mode};
6-
use embassy_time::{with_timeout, Duration, Timer};
76

87
mod config;
98

9+
#[cfg(test)]
10+
mod tests;
11+
12+
cfg_if::cfg_if! {
13+
if #[cfg(not(test))] {
14+
use embassy_time::{with_timeout, Duration, Timer};
15+
} else {
16+
use std::time::Duration;
17+
use tokio::time::timeout as with_timeout;
18+
}
19+
}
20+
1021
/// A generic button that asynchronously detects [`ButtonEvent`]s.
1122
#[derive(Debug, Clone, Copy)]
1223
pub struct Button<P> {
@@ -65,69 +76,80 @@ where
6576
/// **not** be called from tasks where blocking for long periods of time is not desireable.
6677
pub async fn update(&mut self) -> ButtonEvent {
6778
loop {
68-
match self.state {
69-
State::Unknown => {
70-
if self.is_pin_pressed() {
71-
self.state = State::Pressed;
72-
} else {
73-
self.state = State::Idle;
74-
}
79+
if let Some(event) = self.update_step().await {
80+
return event;
81+
}
82+
}
83+
}
84+
85+
async fn update_step(&mut self) -> Option<ButtonEvent> {
86+
match self.state {
87+
State::Unknown => {
88+
if self.is_pin_pressed() {
89+
self.state = State::Pressed;
90+
} else {
91+
self.state = State::Idle;
7592
}
93+
None
94+
}
7695

77-
State::Pressed => {
78-
match with_timeout(self.config.long_press, self.wait_for_release()).await {
79-
Ok(_) => {
80-
// Short press
81-
self.debounce_delay().await;
82-
if self.is_pin_released() {
83-
self.state = State::Released;
84-
}
85-
}
86-
Err(_) => {
87-
// Long press detected
88-
self.count = 0;
89-
self.state = State::PendingRelease;
90-
return ButtonEvent::LongPress;
96+
State::Pressed => {
97+
match with_timeout(self.config.long_press, self.wait_for_release()).await {
98+
Ok(_) => {
99+
// Short press
100+
self.debounce_delay().await;
101+
if self.is_pin_released() {
102+
self.state = State::Released;
91103
}
104+
None
105+
}
106+
Err(_) => {
107+
// Long press detected
108+
self.count = 0;
109+
self.state = State::PendingRelease;
110+
Some(ButtonEvent::LongPress)
92111
}
93112
}
113+
}
94114

95-
State::Released => {
96-
match with_timeout(self.config.double_click, self.wait_for_press()).await {
97-
Ok(_) => {
98-
// Continue sequence
99-
self.debounce_delay().await;
100-
if self.is_pin_pressed() {
101-
self.count += 1;
102-
self.state = State::Pressed;
103-
}
115+
State::Released => {
116+
match with_timeout(self.config.double_click, self.wait_for_press()).await {
117+
Ok(_) => {
118+
// Continue sequence
119+
self.debounce_delay().await;
120+
if self.is_pin_pressed() {
121+
self.count += 1;
122+
self.state = State::Pressed;
104123
}
105-
Err(_) => {
106-
// Sequence ended
107-
let count = self.count;
108-
self.count = 0;
109-
self.state = State::Idle;
110-
return ButtonEvent::ShortPress { count };
111-
}
112-
};
124+
None
125+
}
126+
Err(_) => {
127+
// Sequence ended
128+
let count = self.count;
129+
self.count = 0;
130+
self.state = State::Idle;
131+
Some(ButtonEvent::ShortPress { count })
132+
}
113133
}
134+
}
114135

115-
State::Idle => {
116-
self.wait_for_press().await;
117-
self.debounce_delay().await;
118-
if self.is_pin_pressed() {
119-
self.count = 1;
120-
self.state = State::Pressed;
121-
}
136+
State::Idle => {
137+
self.wait_for_press().await;
138+
self.debounce_delay().await;
139+
if self.is_pin_pressed() {
140+
self.count = 1;
141+
self.state = State::Pressed;
122142
}
143+
None
144+
}
123145

124-
State::PendingRelease => {
125-
self.wait_for_release().await;
126-
self.debounce_delay().await;
127-
if self.is_pin_released() {
128-
self.state = State::Idle;
129-
}
146+
State::PendingRelease => {
147+
self.wait_for_release().await;
148+
self.debounce_delay().await;
149+
if self.is_pin_released() {
150+
self.state = State::Idle;
130151
}
152+
None
131153
}
132154
}
133155
}
@@ -155,6 +177,16 @@ where
155177
}
156178

157179
async fn debounce_delay(&self) {
158-
Timer::after(self.config.debounce).await;
180+
delay(self.config.debounce).await;
181+
}
182+
}
183+
184+
async fn delay(duration: Duration) {
185+
cfg_if::cfg_if! {
186+
if #[cfg(not(test))] {
187+
Timer::after(duration).await;
188+
} else {
189+
tokio::time::sleep(duration).await;
190+
}
159191
}
160192
}

src/tests.rs

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// use std::assert_matches::assert_matches;
2+
3+
use claims::{assert_matches, assert_none, assert_some_eq};
4+
use embedded_hal_mock::eh1::pin::{Mock, State as PinState, Transaction, TransactionKind as Kind};
5+
6+
use crate::{Button, ButtonConfig, ButtonEvent, Duration, Mode, State};
7+
8+
// Use shorter times to speed up test execution
9+
const CONFIG: ButtonConfig = ButtonConfig {
10+
debounce: Duration::from_millis(10),
11+
double_click: Duration::from_millis(100),
12+
long_press: Duration::from_millis(200),
13+
mode: Mode::PullUp,
14+
};
15+
16+
#[tokio::test(start_paused = true)]
17+
async fn start_idle() {
18+
let expectations = [Transaction::new(Kind::Get(PinState::High))];
19+
let pin = Mock::new(&expectations);
20+
let mut button = Button::new(pin, CONFIG);
21+
22+
button.update_step().await;
23+
24+
assert_matches!(button.state, State::Idle);
25+
26+
button.pin.done();
27+
}
28+
29+
#[tokio::test]
30+
async fn start_pressed() {
31+
let expectations = [Transaction::get(PinState::Low)];
32+
let pin = Mock::new(&expectations);
33+
let mut button = Button::new(pin, CONFIG);
34+
35+
let event = button.update_step().await;
36+
assert_none!(event);
37+
assert_matches!(button.state, State::Pressed);
38+
39+
button.pin.done();
40+
}
41+
42+
#[tokio::test]
43+
async fn single_press() {
44+
let expectations = [
45+
Transaction::wait_for_state(PinState::Low, Duration::from_millis(10)),
46+
Transaction::get(PinState::Low),
47+
Transaction::wait_for_state(PinState::High, Duration::from_millis(10)),
48+
Transaction::get(PinState::High),
49+
];
50+
let pin = Mock::new(&expectations);
51+
let mut button = Button::new(pin, CONFIG);
52+
button.state = State::Idle;
53+
54+
let event = button.update_step().await;
55+
assert_none!(event);
56+
assert_matches!(button.state, State::Pressed);
57+
58+
let event = button.update_step().await;
59+
assert_none!(event);
60+
assert_matches!(button.state, State::Released);
61+
assert_eq!(button.count, 1);
62+
63+
button.pin.done();
64+
}
65+
66+
#[tokio::test]
67+
async fn long_press() {
68+
let expectations = [
69+
Transaction::wait_for_state(PinState::Low, Duration::from_millis(10)),
70+
Transaction::get(PinState::Low),
71+
Transaction::wait_for_state(PinState::High, Duration::from_millis(250)),
72+
Transaction::wait_for_state(PinState::High, Duration::from_millis(10)),
73+
Transaction::get(PinState::High),
74+
];
75+
let pin = Mock::new(&expectations);
76+
let mut button = Button::new(pin, CONFIG);
77+
button.state = State::Idle;
78+
79+
let _ = button.update_step().await;
80+
81+
let event = button.update_step().await;
82+
assert_some_eq!(event, ButtonEvent::LongPress);
83+
assert_matches!(button.state, State::PendingRelease);
84+
assert_eq!(button.count, 0);
85+
86+
let event = button.update_step().await;
87+
assert_none!(event);
88+
assert_matches!(button.state, State::Idle);
89+
assert_eq!(button.count, 0);
90+
91+
button.pin.done();
92+
}
93+
94+
#[tokio::test]
95+
async fn debounce_press() {
96+
let expectations = [
97+
Transaction::wait_for_state(PinState::Low, Duration::from_millis(10)),
98+
Transaction::get(PinState::High),
99+
];
100+
let pin = Mock::new(&expectations);
101+
let mut button = Button::new(pin, CONFIG);
102+
button.state = State::Idle;
103+
104+
let event = button.update_step().await;
105+
assert_none!(event);
106+
assert_matches!(button.state, State::Idle);
107+
108+
button.pin.done();
109+
}
110+
111+
#[tokio::test]
112+
async fn debounce_release() {
113+
let expectations = [
114+
Transaction::wait_for_state(PinState::High, Duration::from_millis(10)),
115+
Transaction::get(PinState::Low),
116+
];
117+
let pin = Mock::new(&expectations);
118+
let mut button = Button::new(pin, CONFIG);
119+
button.state = State::Pressed;
120+
121+
let event = button.update_step().await;
122+
assert_none!(event);
123+
assert_matches!(button.state, State::Pressed);
124+
125+
button.pin.done();
126+
}

0 commit comments

Comments
 (0)