diff --git a/Cargo.toml b/Cargo.toml index 1c403e438..e368c8add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "tokio-reactor", "tokio-signal", "tokio-sync", + "tokio-test", "tokio-threadpool", "tokio-timer", "tokio-tcp", diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0102eb6b1..edbba0bcc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -45,6 +45,7 @@ jobs: - tokio-sync - tokio-threadpool - tokio-timer + - tokio-test - tokio-trace - tokio-trace/tokio-trace-core - tokio-trace/test-log-support diff --git a/tokio-test/CHANGELOG.md b/tokio-test/CHANGELOG.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tokio-test/CHANGELOG.md @@ -0,0 +1 @@ + diff --git a/tokio-test/Cargo.toml b/tokio-test/Cargo.toml new file mode 100644 index 000000000..c34d9441a --- /dev/null +++ b/tokio-test/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tokio-test" + +# When releasing to crates.io: +# - Update html_root_url. +# - Update doc url +# - Cargo.toml +# - README.md +# - Update CHANGELOG.md. +# - Create "v0.1.x" git tag. +version = "0.1.0" +authors = ["Tokio Contributors "] +license = "MIT" +repository = "https://github.com/tokio-rs/tokio" +homepage = "https://tokio.rs" +documentation = "https://docs.rs/tokio-test/0.1.0/tokio_test" +description = """ +Testing utilities for Tokio- and futures-based code +""" +categories = ["asynchronous", "testing"] +publish = false + +[dependencies] +futures = "0.1" +tokio-timer = "0.2" +tokio-executor = "0.1" diff --git a/tokio-test/LICENSE b/tokio-test/LICENSE new file mode 100644 index 000000000..cdb28b4b5 --- /dev/null +++ b/tokio-test/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2019 Tokio Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/tokio-test/README.md b/tokio-test/README.md new file mode 100644 index 000000000..cc737fb44 --- /dev/null +++ b/tokio-test/README.md @@ -0,0 +1,36 @@ +# tokio-test + +Tokio and Futures based testing utilities + +[Documenation](https://docs.rs/tokio-test) + +## Usage + +First, add this to your `Cargo.toml`: + +```toml +[dev-dependencies] +tokio-test = "0.1.0" +``` + +Next, add this to your crate: + +```rust +#[macro_use] +extern crate tokio_test; +``` + +You can find extensive documentation and examples about how to use this crate +online at [https://tokio.rs](https://tokio.rs). The [API +documentation](https://docs.rs/tokio-test) is also a great place to get started +for the nitty-gritty. + +## License + +This project is licensed under the [MIT license](LICENSE). + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Tokio by you, shall be licensed as MIT, without any additional +terms or conditions. diff --git a/tokio-test/src/clock.rs b/tokio-test/src/clock.rs new file mode 100644 index 000000000..f8e964abc --- /dev/null +++ b/tokio-test/src/clock.rs @@ -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: 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(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, + time: MockTime, +} + +type Inner = Arc>; + +#[derive(Debug, Clone)] +struct MockTime { + inner: Inner, + _pd: PhantomData>, +} + +#[derive(Debug)] +struct MockNow { + inner: Inner, +} + +#[derive(Debug)] +struct MockPark { + inner: Inner, + _pd: PhantomData>, +} + +#[derive(Debug)] +struct MockUnpark { + inner: Inner, +} + +#[derive(Debug)] +struct State { + base: Instant, + advance: Duration, + unparked: bool, + park_for: Option, +} + +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(&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, time: MockTime) -> Self { + Handle { timer, time } + } + + /// Turn the internal timer and mock park for the provided duration. + pub fn turn(&mut self, duration: Option) { + 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() + } +} diff --git a/tokio-test/src/lib.rs b/tokio-test/src/lib.rs new file mode 100644 index 000000000..262dca5c8 --- /dev/null +++ b/tokio-test/src/lib.rs @@ -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; diff --git a/tokio-test/src/macros.rs b/tokio-test/src/macros.rs new file mode 100644 index 000000000..f584971e0 --- /dev/null +++ b/tokio-test/src/macros.rs @@ -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(), ()); + } + +} diff --git a/tokio-test/src/task.rs b/tokio-test/src/task.rs new file mode 100644 index 000000000..f3d52c3dd --- /dev/null +++ b/tokio-test/src/task.rs @@ -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, +} + +#[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(&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(); + } +} diff --git a/tokio-test/tests/clock.rs b/tokio-test/tests/clock.rs new file mode 100644 index 000000000..6690a7cb9 --- /dev/null +++ b/tokio-test/tests/clock.rs @@ -0,0 +1,44 @@ +#[macro_use] +extern crate tokio_test; +extern crate futures; +extern crate tokio_timer; + +use futures::Future; +use std::time::{Duration, Instant}; +use tokio_test::clock::MockClock; +use tokio_test::task::MockTask; +use tokio_timer::Delay; + +#[test] +fn clock() { + let mut mock = MockClock::new(); + + mock.enter(|handle| { + let deadline = Instant::now() + Duration::from_secs(1); + let mut delay = Delay::new(deadline); + + assert_not_ready!(delay.poll()); + + handle.advance(Duration::from_secs(2)); + + assert_ready!(delay.poll()); + }); +} + +#[test] +fn notify() { + let deadline = Instant::now() + Duration::from_secs(1); + let mut mock = MockClock::new(); + let mut task = MockTask::new(); + + mock.enter(|handle| { + let mut delay = Delay::new(deadline); + + task.enter(|| assert_not_ready!(delay.poll())); + + handle.advance(Duration::from_secs(1)); + + assert!(task.is_notified()); + assert_ready!(delay.poll()); + }); +}