Skip to content

Commit

Permalink
Merge pull request #40 from niclashoyer/timers
Browse files Browse the repository at this point in the history
Add mock for timers
  • Loading branch information
dbrgn committed Jan 6, 2023
2 parents 76a7eb5 + 3492b68 commit bfa658d
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 13 deletions.
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
toolchain:
- "1.56"
- "1.60"
- "nightly"
steps:
- uses: actions/checkout@v3
Expand All @@ -35,19 +35,20 @@ jobs:
rustc --version
cargo --version
# Build main crate
- name: Build
run: cargo build
- name: Build (all features)
if: ${{ matrix.toolchain == 'nightly' }}
run: cargo build --all-features
# Check crate
- name: Check
run: cargo check
- name: Check (no default features)
run: cargo check --no-default-features
- name: Check (all features)
run: cargo check --all-features

# Test main crate
# Test crate
- name: Test
if: ${{ matrix.toolchain != 'nightly' }}
run: cargo test
- name: Test (no default features)
run: cargo test --no-default-features
- name: Test (all features)
if: ${{ matrix.toolchain == 'nightly' }}
run: cargo test --all-features

# Check code formatting
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ edition = "2021"

[dependencies]
embedded-hal = { version = "0.2.7", features = ["unproven"] }
embedded-time = { version = "0.12", optional = true }
void = { version = "^1.0", optional = true }
nb = "0.1.1"

[features]
default = ["embedded-time"]
embedded-time = ["dep:embedded-time", "dep:void"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ or no-op implementations are used.
The goal of the crate is to be able to test drivers in CI without having access
to hardware.

This crate requires Rust 1.56+!
This crate requires Rust 1.60+!

[Docs](https://docs.rs/embedded-hal-mock/)

Expand All @@ -32,7 +32,7 @@ This crate requires Rust 1.56+!
- [ ] RNG
- [x] I/O pins (including PWM)
- [x] ADC
- [ ] Timers
- [x] Timers (with `embedded-time` Cargo feature)
- [ ] ...

Pull requests for more mock implementations are welcome! :)
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
msrv = "1.56"
msrv = "1.60"
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
//!
//! See module-level docs for more information.
//!
//! ## Cargo Features
//!
//! There are currently the following cargo features:
//!
//! - `embedded-time`: Enable the `timer` module (enabled by default)
//!
//! ## no\_std
//!
//! Currently this crate is not `no_std`. If you think this is important, let
Expand All @@ -27,3 +33,5 @@ pub mod i2c;
pub mod pin;
pub mod serial;
pub mod spi;
#[cfg(feature = "embedded-time")]
pub mod timer;
160 changes: 160 additions & 0 deletions src/timer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Provides a mocked [embedded_time::Clock] that can be used for host-side testing
//! crates that use [embedded_hal::timer].
//!
//! The provided [embedded_time::Clock] implementation is thread safe and can be freely
//! skipped forward with nanosecond precision.
//!
//! # Usage
//!
//! ```rust
//! use embedded_hal::timer::CountDown;
//! use embedded_time::duration::*;
//! use embedded_hal_mock::timer::MockClock;
//!
//! let mut clock = MockClock::new();
//! let mut timer = clock.get_timer();
//! timer.start(100.nanoseconds());
//! // hand over timer to embedded-hal based driver
//! // continue to tick clock
//! clock.tick(50.nanoseconds());
//! assert_eq!(timer.wait(), Err(nb::Error::WouldBlock));
//! clock.tick(50.nanoseconds());
//! assert_eq!(timer.wait(), Ok(()));
//! clock.tick(50.nanoseconds());
//! assert_eq!(timer.wait(), Err(nb::Error::WouldBlock));
//! clock.tick(50.nanoseconds());
//! assert_eq!(timer.wait(), Ok(()));
//! ```

use std::{
convert::Infallible,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use void::Void;

use embedded_hal::timer::{Cancel, CountDown, Periodic};
pub use embedded_time::Clock;
use embedded_time::{clock, duration::*, fraction::Fraction, Instant};

/// A simulated clock that can be used in tests.
#[derive(Clone, Debug)]
pub struct MockClock {
ticks: Arc<AtomicU64>,
}

impl Clock for MockClock {
type T = u64;
const SCALING_FACTOR: Fraction = Fraction::new(1, 1_000_000_000);

fn try_now(&self) -> Result<Instant<Self>, clock::Error> {
let ticks: u64 = self.ticks.load(Ordering::Relaxed);
Ok(Instant::<Self>::new(ticks))
}
}

impl Default for MockClock {
fn default() -> Self {
MockClock {
ticks: Arc::new(AtomicU64::new(0)),
}
}
}

impl MockClock {
/// Creates a new simulated clock.
pub fn new() -> Self {
Self::default()
}

/// Returns the number of elapsed nanoseconds.
pub fn elapsed(&self) -> Nanoseconds<u64> {
Nanoseconds(self.ticks.load(Ordering::Relaxed))
}

/// Forward the clock by `ticks` amount.
pub fn tick<T>(&mut self, ticks: T)
where
T: Into<Nanoseconds<u64>>,
{
self.ticks.fetch_add(ticks.into().0, Ordering::Relaxed);
}

/// Get a new timer based on the clock.
pub fn get_timer(&self) -> MockTimer {
let clock = self.clone();
let duration = Nanoseconds(1);
let expiration = clock.try_now().unwrap();
MockTimer {
clock: self.clone(),
duration,
expiration,
started: false,
}
}
}

/// A simulated timer that can be used in tests.
pub struct MockTimer {
clock: MockClock,
duration: Nanoseconds<u64>,
expiration: Instant<MockClock>,
started: bool,
}

impl CountDown for MockTimer {
type Time = Nanoseconds<u64>;

fn start<T>(&mut self, count: T)
where
T: Into<Self::Time>,
{
let now = self.clock.try_now().unwrap();
self.duration = count.into();
self.expiration = now + self.duration;
self.started = true;
}

fn wait(&mut self) -> nb::Result<(), Void> {
let now = self.clock.try_now().unwrap();
if self.started && now >= self.expiration {
self.expiration = now + self.duration;
Ok(())
} else {
Err(nb::Error::WouldBlock)
}
}
}

impl Periodic for MockTimer {}

impl Cancel for MockTimer {
type Error = Infallible;

fn cancel(&mut self) -> Result<(), Self::Error> {
self.started = false;
Ok(())
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn count_down() {
let mut clock = MockClock::new();
let mut timer = clock.get_timer();
timer.start(100.nanoseconds());
clock.tick(50.nanoseconds());
assert_eq!(timer.wait(), Err(nb::Error::WouldBlock));
clock.tick(50.nanoseconds());
assert_eq!(timer.wait(), Ok(()));
clock.tick(50.nanoseconds());
assert_eq!(timer.wait(), Err(nb::Error::WouldBlock));
clock.tick(50.nanoseconds());
assert_eq!(timer.wait(), Ok(()));
}
}

0 comments on commit bfa658d

Please sign in to comment.