Skip to content

Commit

Permalink
Merge pull request #1176 from Nemo157/testing-utilities
Browse files Browse the repository at this point in the history
Initial testing utilities
  • Loading branch information
MajorBreakfast authored Aug 17, 2018
2 parents e4a963f + e3db5e6 commit 8fb41ab
Show file tree
Hide file tree
Showing 41 changed files with 862 additions and 299 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ matrix:
- cargo build --manifest-path futures-io/Cargo.toml --all-features
- cargo build --manifest-path futures-sink/Cargo.toml --all-features
- cargo build --manifest-path futures-util/Cargo.toml --all-features
- cargo build --manifest-path futures-test/Cargo.toml --all-features

- name: cargo build --all-features (with minimal versions)
rust: nightly
Expand Down Expand Up @@ -81,6 +82,7 @@ matrix:
- RUSTDOCFLAGS=-Dwarnings cargo doc --all
--exclude futures-preview
--exclude futures-executor-preview
--exclude futures-test-preview
- cargo doc

script:
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ members = [
"futures-io",
"futures-sink",
"futures-util",
"futures-test",
]
26 changes: 26 additions & 0 deletions futures-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
cargo-features = ["edition"]

[package]
name = "futures-test-preview"
edition = "2018"
version = "0.3.0-alpha.3"
authors = ["Wim Looman <wim@nemo157.com>"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/rust-lang-nursery/futures-rs"
homepage = "https://rust-lang-nursery.github.io/futures-rs"
documentation = "https://rust-lang-nursery.github.io/futures-doc/0.3.0-alpha.3/futures_test"
description = """
Common utilities for testing components built off futures-rs.
"""

[lib]
name = "futures_test"

[dependencies]
futures-core-preview = { version = "0.3.0-alpha.2", path = "../futures-core", default-features = false }
futures-util-preview = { version = "0.3.0-alpha.2", path = "../futures-util", default-features = false }
futures-executor-preview = { version = "0.3.0-alpha.2", path = "../futures-executor", default-features = false }
pin-utils = { version = "0.1.0-alpha.1", default-features = false }

[dev-dependencies]
futures-preview = { version = "0.3.0-alpha.2", path = "../futures", default-features = false, features = ["std"] }
1 change: 1 addition & 0 deletions futures-test/LICENSE-APACHE
1 change: 1 addition & 0 deletions futures-test/LICENSE-MIT
125 changes: 125 additions & 0 deletions futures-test/src/assert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use futures_core::stream::Stream;
use std::marker::Unpin;

#[doc(hidden)]
pub fn assert_is_unpin_stream<S: Stream + Unpin>(_: &mut S) {}

/// Assert that the next poll to the provided stream will return
/// [`Poll::Pending`](futures_core::task::Poll::Pending).
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::stream;
/// use futures_test::future::FutureTestExt;
/// use futures_test::{
/// assert_stream_pending, assert_stream_next, assert_stream_done,
/// };
/// use pin_utils::pin_mut;
///
/// let mut stream = stream::once((async { 5 }).delay());
/// pin_mut!(stream);
///
/// assert_stream_pending!(stream);
/// assert_stream_next!(stream, 5);
/// assert_stream_done!(stream);
/// ```
#[macro_export]
macro_rules! assert_stream_pending {
($stream:expr) => {{
let mut stream = &mut $stream;
$crate::assert::assert_is_unpin_stream(stream);
let stream = $crate::std_reexport::mem::PinMut::new(stream);
let cx = &mut $crate::task::no_spawn_context();
let poll = $crate::futures_core_reexport::stream::Stream::poll_next(
stream, cx,
);
if poll.is_ready() {
panic!("assertion failed: stream is not pending");
}
}};
}

/// Assert that the next poll to the provided stream will return
/// [`Poll::Ready`](futures_core::task::Poll::Ready) with the provided item.
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::stream;
/// use futures_test::future::FutureTestExt;
/// use futures_test::{
/// assert_stream_pending, assert_stream_next, assert_stream_done,
/// };
/// use pin_utils::pin_mut;
///
/// let mut stream = stream::once((async { 5 }).delay());
/// pin_mut!(stream);
///
/// assert_stream_pending!(stream);
/// assert_stream_next!(stream, 5);
/// assert_stream_done!(stream);
/// ```
#[macro_export]
macro_rules! assert_stream_next {
($stream:expr, $item:expr) => {{
let mut stream = &mut $stream;
$crate::assert::assert_is_unpin_stream(stream);
let stream = $crate::std_reexport::mem::PinMut::new(stream);
let cx = &mut $crate::task::no_spawn_context();
match $crate::futures_core_reexport::stream::Stream::poll_next(stream, cx) {
$crate::futures_core_reexport::task::Poll::Ready(Some(x)) => {
assert_eq!(x, $item);
}
$crate::futures_core_reexport::task::Poll::Ready(None) => {
panic!("assertion failed: expected stream to provide item but stream is at its end");
}
$crate::futures_core_reexport::task::Poll::Pending => {
panic!("assertion failed: expected stream to provide item but stream wasn't ready");
}
}
}}
}

/// Assert that the next poll to the provided stream will return an empty
/// [`Poll::Ready`](futures_core::task::Poll::Ready) signalling the
/// completion of the stream.
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::stream;
/// use futures_test::future::FutureTestExt;
/// use futures_test::{
/// assert_stream_pending, assert_stream_next, assert_stream_done,
/// };
/// use pin_utils::pin_mut;
///
/// let mut stream = stream::once((async { 5 }).delay());
/// pin_mut!(stream);
///
/// assert_stream_pending!(stream);
/// assert_stream_next!(stream, 5);
/// assert_stream_done!(stream);
/// ```
#[macro_export]
macro_rules! assert_stream_done {
($stream:expr) => {{
let mut stream = &mut $stream;
$crate::assert::assert_is_unpin_stream(stream);
let stream = $crate::std_reexport::mem::PinMut::new(stream);
let cx = &mut $crate::task::no_spawn_context();
match $crate::futures_core_reexport::stream::Stream::poll_next(stream, cx) {
$crate::futures_core_reexport::task::Poll::Ready(Some(_)) => {
panic!("assertion failed: expected stream to be done but had more elements");
}
$crate::futures_core_reexport::task::Poll::Ready(None) => {}
$crate::futures_core_reexport::task::Poll::Pending => {
panic!("assertion failed: expected stream to be done but was pending");
}
}
}}
}
45 changes: 45 additions & 0 deletions futures-test/src/future/delay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use futures_core::future::Future;
use futures_core::task::{self, Poll};
use std::mem::PinMut;
use pin_utils::{unsafe_pinned, unsafe_unpinned};

/// Combinator that guarantees one [`Poll::Pending`] before polling its inner
/// future.
///
/// This is created by the [`FutureTestExt::delay`](super::FutureTestExt::delay)
/// method.
#[derive(Debug)]
#[must_use = "futures do nothing unless polled"]
pub struct Delayed<Fut: Future> {
future: Fut,
polled_before: bool,
}

impl<Fut: Future> Delayed<Fut> {
unsafe_pinned!(future: Fut);
unsafe_unpinned!(polled_before: bool);

pub(super) fn new(future: Fut) -> Self {
Self {
future,
polled_before: false,
}
}
}

impl<Fut: Future> Future for Delayed<Fut> {
type Output = Fut::Output;

fn poll(
mut self: PinMut<Self>,
cx: &mut task::Context,
) -> Poll<Self::Output> {
if *self.polled_before() {
self.future().poll(cx)
} else {
*self.polled_before() = true;
cx.waker().wake();
Poll::Pending
}
}
}
65 changes: 65 additions & 0 deletions futures-test/src/future/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Additional combinators for testing futures.

mod delay;

use self::delay::Delayed;
use futures_core::future::Future;
use futures_executor;
use std::thread;

/// Additional combinators for testing futures.
pub trait FutureTestExt: Future {
/// Introduces one [`Poll::Pending`](futures_core::task::Poll::Pending)
/// before polling the given future
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::task::Poll;
/// use futures::future::FutureExt;
/// use futures_test::task;
/// use futures_test::future::FutureTestExt;
/// use pin_utils::pin_mut;
///
/// let future = (async { 5 }).delay();
/// pin_mut!(future);
///
/// let cx = &mut task::no_spawn_context();
///
/// assert_eq!(future.poll_unpin(cx), Poll::Pending);
/// assert_eq!(future.poll_unpin(cx), Poll::Ready(5));
/// ```
fn delay(self) -> Delayed<Self>
where
Self: Sized,
{
delay::Delayed::new(self)
}

/// Runs this future on a dedicated executor running in a background thread.
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::channel::oneshot;
/// use futures::executor::block_on;
/// use futures_test::future::FutureTestExt;
///
/// let (tx, rx) = oneshot::channel::<i32>();
///
/// (async { tx.send(5).unwrap() }).run_in_background();
///
/// assert_eq!(block_on(rx), Ok(5));
/// ```
fn run_in_background(self)
where
Self: Sized + Send + 'static,
Self::Output: Send,
{
thread::spawn(|| futures_executor::block_on(self));
}
}

impl<Fut> FutureTestExt for Fut where Fut: Future {}
28 changes: 28 additions & 0 deletions futures-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Utilities to make testing [`Future`s](futures_core::Future) easier

#![feature(
arbitrary_self_types,
async_await,
await_macro,
futures_api,
pin,
)]
#![warn(missing_docs, missing_debug_implementations)]
#![deny(bare_trait_objects)]
#![doc(
html_root_url = "https://rust-lang-nursery.github.io/futures-doc/0.3.0-alpha.3/futures_test"
)]

#[doc(hidden)]
pub use std as std_reexport;

#[doc(hidden)]
pub extern crate futures_core as futures_core_reexport;

#[macro_use]
#[doc(hidden)]
pub mod assert;

pub mod task;

pub mod future;
65 changes: 65 additions & 0 deletions futures-test/src/task/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::task::{spawn, wake};
use futures_core::task::Context;

/// Create a new [`task::Context`](futures_core::task::Context) where both
/// the [`waker`](futures_core::task::Context::waker) and
/// [`spawner`](futures_core::task::Context::spawner) will panic if used.
///
/// # Examples
///
/// ```should_panic
/// #![feature(futures_api)]
/// use futures_test::task;
///
/// let cx = task::panic_context();
/// cx.waker().wake(); // Will panic
/// ```
pub fn panic_context() -> Context<'static> {
Context::new(wake::panic_local_waker_ref(), spawn::panic_mut())
}

/// Create a new [`task::Context`](futures_core::task::Context) where the
/// [`waker`](futures_core::task::Context::waker) will ignore any calls to
/// `wake` while the [`spawner`](futures_core::task::Context::spawner) will
/// panic if used.
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::future::Future;
/// use futures::task::Poll;
/// use futures_test::task::no_spawn_context;
/// use pin_utils::pin_mut;
///
/// let mut future = async { 5 };
/// pin_mut!(future);
///
/// assert_eq!(future.poll(&mut no_spawn_context()), Poll::Ready(5));
/// ```
pub fn no_spawn_context() -> Context<'static> {
Context::new(wake::noop_local_waker_ref(), spawn::panic_mut())
}

/// Create a new [`task::Context`](futures_core::task::Context) where the
/// [`waker`](futures_core::task::Context::waker) and
/// [`spawner`](futures_core::task::Context::spawner) will both ignore any
/// uses.
///
/// # Examples
///
/// ```
/// #![feature(async_await, futures_api, pin)]
/// use futures::future::Future;
/// use futures::task::Poll;
/// use futures_test::task::noop_context;
/// use pin_utils::pin_mut;
///
/// let mut future = async { 5 };
/// pin_mut!(future);
///
/// assert_eq!(future.poll(&mut noop_context()), Poll::Ready(5));
/// ```
pub fn noop_context() -> Context<'static> {
Context::new(wake::noop_local_waker_ref(), spawn::noop_mut())
}
Loading

0 comments on commit 8fb41ab

Please sign in to comment.