Introduce tokio-test crate (#1030)

This commit is contained in:
Lucio Franco
2019-04-23 20:17:57 -07:00
committed by Carl Lerche
parent 62f34e15ce
commit e5cf0cc717
11 changed files with 671 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
//! A mocked clock for use with `tokio_timer` based futures.
//!
//! # Example
//!
//! ```
//! # #[macro_use] extern crate tokio_test;
//! # extern crate futures;
//! # extern crate tokio_timer;
//! # use tokio_test::clock;
//! # use tokio_timer::Delay;
//! # use std::time::Duration;
//! # use futures::Future;
//! clock::mock(|handle| {
//! let mut delay = Delay::new(handle.now() + Duration::from_secs(1));
//!
//! assert_not_ready!(delay.poll());
//!
//! handle.advance(Duration::from_secs(1));
//!
//! assert_ready!(delay.poll());
//! });
//! ```
use futures::{future::lazy, Future};
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio_executor::park::{Park, Unpark};
use tokio_timer::clock::{Clock, Now};
use tokio_timer::Timer;
/// Run the provided closure with a `MockClock` that starts at the current time.
pub fn mock<F, R>(f: F) -> R
where
F: FnOnce(&mut Handle) -> R,
{
let mut mock = MockClock::new();
mock.enter(f)
}
/// Run the provided closure with a `MockClock` that starts at the provided `Instant`.
pub fn mock_at<F, R>(instant: Instant, f: F) -> R
where
F: FnOnce(&mut Handle) -> R,
{
let mut mock = MockClock::with_instant(instant);
mock.enter(f)
}
/// Mock clock for use with `tokio-timer` futures.
///
/// A mock timer that is able to advance and wake after a
/// certain duration.
#[derive(Debug)]
pub struct MockClock {
time: MockTime,
clock: Clock,
}
/// A handle to the `MockClock`.
#[derive(Debug)]
pub struct Handle {
timer: Timer<MockPark>,
time: MockTime,
}
type Inner = Arc<Mutex<State>>;
#[derive(Debug, Clone)]
struct MockTime {
inner: Inner,
_pd: PhantomData<Rc<()>>,
}
#[derive(Debug)]
struct MockNow {
inner: Inner,
}
#[derive(Debug)]
struct MockPark {
inner: Inner,
_pd: PhantomData<Rc<()>>,
}
#[derive(Debug)]
struct MockUnpark {
inner: Inner,
}
#[derive(Debug)]
struct State {
base: Instant,
advance: Duration,
unparked: bool,
park_for: Option<Duration>,
}
impl MockClock {
/// Create a new `MockClock` with the current time.
pub fn new() -> Self {
MockClock::with_instant(Instant::now())
}
/// Create a `MockClock` with its current time at a duration from now
///
/// This will create a clock with `Instant::now() + duration` as the current time.
pub fn with_duration(duration: Duration) -> Self {
let instant = Instant::now() + duration;
MockClock::with_instant(instant)
}
/// Create a `MockClock` that sets its current time as the `Instant` provided.
pub fn with_instant(instant: Instant) -> Self {
let time = MockTime::new(instant);
let clock = Clock::new_with_now(time.mock_now());
MockClock { time, clock }
}
/// Enter the `MockClock` context.
pub fn enter<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Handle) -> R,
{
let mut enter = ::tokio_executor::enter().unwrap();
::tokio_timer::clock::with_default(&self.clock, &mut enter, |enter| {
let park = self.time.mock_park();
let timer = Timer::new(park);
let handle = timer.handle();
let time = self.time.clone();
::tokio_timer::with_default(&handle, enter, |_| {
let mut handle = Handle::new(timer, time);
lazy(|| Ok::<_, ()>(f(&mut handle))).wait().unwrap()
})
})
}
}
impl Handle {
pub(self) fn new(timer: Timer<MockPark>, time: MockTime) -> Self {
Handle { timer, time }
}
/// Turn the internal timer and mock park for the provided duration.
pub fn turn(&mut self, duration: Option<Duration>) {
self.timer.turn(duration).unwrap();
}
/// Advance the `MockClock` by the provided duration.
pub fn advance(&mut self, duration: Duration) {
let inner = self.timer.get_park().inner.clone();
let deadline = inner.lock().unwrap().now() + duration;
while inner.lock().unwrap().now() < deadline {
let dur = deadline - inner.lock().unwrap().now();
self.turn(Some(dur));
}
}
/// Get the currently mocked time
pub fn now(&mut self) -> Instant {
self.time.now()
}
}
impl MockTime {
pub(crate) fn new(now: Instant) -> MockTime {
let state = State {
base: now,
advance: Duration::default(),
unparked: false,
park_for: None,
};
MockTime {
inner: Arc::new(Mutex::new(state)),
_pd: PhantomData,
}
}
pub(crate) fn mock_now(&self) -> MockNow {
let inner = self.inner.clone();
MockNow { inner }
}
pub(crate) fn mock_park(&self) -> MockPark {
let inner = self.inner.clone();
MockPark {
inner,
_pd: PhantomData,
}
}
pub(crate) fn now(&self) -> Instant {
self.inner.lock().unwrap().now()
}
}
impl State {
fn now(&self) -> Instant {
self.base + self.advance
}
fn advance(&mut self, duration: Duration) {
self.advance += duration;
}
}
impl Park for MockPark {
type Unpark = MockUnpark;
type Error = ();
fn unpark(&self) -> Self::Unpark {
let inner = self.inner.clone();
MockUnpark { inner }
}
fn park(&mut self) -> Result<(), Self::Error> {
let mut inner = self.inner.lock().map_err(|_| ())?;
let duration = inner.park_for.take().expect("call park_for first");
inner.advance(duration);
Ok(())
}
fn park_timeout(&mut self, duration: Duration) -> Result<(), Self::Error> {
let mut inner = self.inner.lock().unwrap();
if let Some(duration) = inner.park_for.take() {
inner.advance(duration);
} else {
inner.advance(duration);
}
Ok(())
}
}
impl Unpark for MockUnpark {
fn unpark(&self) {
if let Ok(mut inner) = self.inner.lock() {
inner.unparked = true;
}
}
}
impl Now for MockNow {
fn now(&self) -> Instant {
self.inner.lock().unwrap().now()
}
}
+23
View File
@@ -0,0 +1,23 @@
#![doc(html_root_url = "https://docs.rs/tokio-test/0.1.0")]
#![deny(missing_docs, missing_debug_implementations, unreachable_pub)]
#![cfg_attr(test, deny(warnings))]
//! Tokio and Futures based testing utilites
//!
//! # Example
//!
//! ```
//! # extern crate futures;
//! # #[macro_use] extern crate tokio_test;
//! # use futures::{Future, future};
//! let mut fut = future::ok::<(), ()>(());
//! assert_ready!(fut.poll());
//! ```
extern crate futures;
extern crate tokio_executor;
extern crate tokio_timer;
pub mod clock;
mod macros;
pub mod task;
+125
View File
@@ -0,0 +1,125 @@
//! A collection of useful macros for testing futures and tokio based code
/// Assert if a poll is ready
#[macro_export]
macro_rules! assert_ready {
($e:expr) => {{
match $e {
Ok(::futures::Async::Ready(v)) => v,
Ok(_) => panic!("not ready"),
Err(e) => panic!("error = {:?}", e),
}
}};
($e:expr, $($msg:tt),+) => {{
match $e {
Ok(::futures::Async::Ready(v)) => v,
Ok(_) => {
let msg = format_args!($($msg),+);
panic!("not ready; {}", msg)
}
Err(e) => {
let msg = format!($($msg),+);
panic!("error = {:?}; {}", e, msg)
}
}
}};
}
/// Asset if the poll is not ready
#[macro_export]
macro_rules! assert_not_ready {
($e:expr) => {{
match $e {
Ok(::futures::Async::NotReady) => {}
Ok(::futures::Async::Ready(v)) => panic!("ready; value = {:?}", v),
Err(e) => panic!("error = {:?}", e),
}
}};
($e:expr, $($msg:tt),+) => {{
match $e {
Ok(::futures::Async::NotReady) => {}
Ok(::futures::Async::Ready(v)) => {
let msg = format_args!($($msg),+);
panic!("ready; value = {:?}; {}", v, msg)
}
Err(e) => {
let msg = format_args!($($msg),+);
panic!("error = {:?}; {}", e, msg)
}
}
}};
}
/// Assert if a poll is ready and check for equality on the value
#[macro_export]
macro_rules! assert_ready_eq {
($e:expr, $expect:expr) => {
match $e {
Ok(e) => assert_eq!(e, ::futures::Async::Ready($expect)),
Err(e) => panic!("error = {:?}", e),
}
};
($e:expr, $expect:expr, $($msg:tt),+) => {
match $e {
Ok(e) => assert_eq!(e, ::futures::Async::Ready($expect), $($msg)+),
Err(e) => {
let msg = format_args!($($msg),+);
panic!("error = {:?}; {}", e, msg)
}
}
};
}
/// Assert if the deadline has passed
#[macro_export]
macro_rules! assert_elapsed {
($e:expr) => {
assert!($e.unwrap_err().is_elapsed());
};
($e:expr, $($msg:expr),+) => {
assert!($e.unwrap_err().is_elapsed(), $msg);
};
}
#[cfg(test)]
mod tests {
use futures::{future, Async, Future, Poll};
#[test]
fn assert_ready() {
let mut fut = future::ok::<(), ()>(());
assert_ready!(fut.poll());
let mut fut = future::ok::<(), ()>(());
assert_ready!(fut.poll(), "some message");
}
#[test]
#[should_panic]
fn assert_ready_err() {
let mut fut = future::err::<(), ()>(());
assert_ready!(fut.poll());
}
#[test]
fn assert_not_ready() {
let poll: Poll<(), ()> = Ok(Async::NotReady);
assert_not_ready!(poll);
assert_not_ready!(poll, "some message");
}
#[test]
#[should_panic]
fn assert_not_ready_err() {
let mut fut = future::err::<(), ()>(());
assert_not_ready!(fut.poll());
}
#[test]
fn assert_ready_eq() {
let mut fut = future::ok::<(), ()>(());
assert_ready_eq!(fut.poll(), ());
}
}
+133
View File
@@ -0,0 +1,133 @@
//! Futures task based helpers
//!
//! # Example
//!
//! This example will use the `MockTask` to set the current task on
//! poll.
//!
//! ```
//! # #[macro_use] extern crate tokio_test;
//! # extern crate futures;
//! # use tokio_test::task::MockTask;
//! # use futures::{sync::mpsc, Stream, Sink, Future, Async};
//! let mut task = MockTask::new();
//! let (tx, mut rx) = mpsc::channel(5);
//!
//! tx.send(()).wait();
//!
//! assert_ready_eq!(task.enter(|| rx.poll()), Some(()));
//! ```
use futures::executor::{spawn, Notify};
use futures::{future, Async};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
/// Mock task
///
/// A mock task is able to intercept and track notifications.
#[derive(Debug)]
pub struct MockTask {
notify: Arc<ThreadNotify>,
}
#[derive(Debug)]
struct ThreadNotify {
state: AtomicUsize,
mutex: Mutex<()>,
condvar: Condvar,
}
const IDLE: usize = 0;
const NOTIFY: usize = 1;
const SLEEP: usize = 2;
impl MockTask {
/// Create a new mock task
pub fn new() -> Self {
MockTask {
notify: Arc::new(ThreadNotify::new()),
}
}
/// Run a closure from the context of the task.
///
/// Any notifications resulting from the execution of the closure are
/// tracked.
pub fn enter<F, R>(&mut self, f: F) -> R
where
F: FnOnce() -> R,
{
self.notify.clear();
let res = spawn(future::lazy(|| Ok::<_, ()>(f()))).poll_future_notify(&self.notify, 0);
match res.unwrap() {
Async::Ready(v) => v,
_ => unreachable!(),
}
}
/// Returns `true` if the inner future has received a readiness notification
/// since the last call to `enter`.
pub fn is_notified(&self) -> bool {
self.notify.is_notified()
}
/// Returns the number of references to the task notifier
///
/// The task itself holds a reference. The return value will never be zero.
pub fn notifier_ref_count(&self) -> usize {
Arc::strong_count(&self.notify)
}
}
impl ThreadNotify {
fn new() -> Self {
ThreadNotify {
state: AtomicUsize::new(IDLE),
mutex: Mutex::new(()),
condvar: Condvar::new(),
}
}
/// Clears any previously received notify, avoiding potential spurrious
/// notifications. This should only be called immediately before running the
/// task.
fn clear(&self) {
self.state.store(IDLE, Ordering::SeqCst);
}
fn is_notified(&self) -> bool {
match self.state.load(Ordering::SeqCst) {
IDLE => false,
NOTIFY => true,
_ => unreachable!(),
}
}
}
impl Notify for ThreadNotify {
fn notify(&self, _unpark_id: usize) {
// First, try transitioning from IDLE -> NOTIFY, this does not require a
// lock.
match self.state.compare_and_swap(IDLE, NOTIFY, Ordering::SeqCst) {
IDLE | NOTIFY => return,
SLEEP => {}
_ => unreachable!(),
}
// The other half is sleeping, this requires a lock
let _m = self.mutex.lock().unwrap();
// Transition from SLEEP -> NOTIFY
match self.state.compare_and_swap(SLEEP, NOTIFY, Ordering::SeqCst) {
SLEEP => {}
_ => return,
}
// Wakeup the sleeper
self.condvar.notify_one();
}
}