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
+1
View File
@@ -12,6 +12,7 @@ members = [
"tokio-reactor",
"tokio-signal",
"tokio-sync",
"tokio-test",
"tokio-threadpool",
"tokio-timer",
"tokio-tcp",
+1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
+26
View File
@@ -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 <[email protected]>"]
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"
+25
View File
@@ -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.
+36
View File
@@ -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.
+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();
}
}
+44
View File
@@ -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());
});
}