Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial testing utilities #1176

Merged
merged 3 commits into from
Aug 17, 2018
Merged

Conversation

Nemo157
Copy link
Member

@Nemo157 Nemo157 commented Aug 4, 2018

I'm not a massive fan of how the generics worked out here, especially around with_context needing two _, _ at the end... any suggestion on a more ergonomic API?

CC #1169

@MajorBreakfast
Copy link
Contributor

I'd prefer the name futures-test because the built-in test crate (contains benchmarking stuff) also uses that name.

use crate::task::{executor, wake};
use futures_core::{
future::Future,
task::{local_waker, Context, Executor, Poll, Wake},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • For consistency with the rest of our codebase the non-nested style would be better
  • Context should not be imported directly. Instead it should always be referred to as task::Context. This decision was made in the 0.2 days. I wasn't around back then, but I think it's because the word "context" is so nondescript.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case it's better to use a bare Context, this is explicitly a helper for managing that exact type. I think I'll change what's used in the docs though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of non-nested style, what about

use crate::task::{executor, wake};
use std::{marker::Unpin, mem::PinMut, sync::Arc};

I assume that the first is definitely ok, but what about the second? What's the actual limit on nesting that you want to enforce?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to expand the second one as well. I'd like to align our code style with the code style of the standard library and compiler. I browsed the code just now and it seems that they consistently expand such things. Also, our existing code does so as well:

use core::marker::Unpin;
use core::mem::PinMut;

can be found many many times in futures-rs 🙂

Copy link
Member Author

@Nemo157 Nemo157 Aug 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, so nesting the final segment in a path is ok, but no more than that.

(I assume the main reason that the compiler expands everything is that being able to nest more than a single segment is relatively recent, only since March)

@MajorBreakfast
Copy link
Contributor

I think the symlinked license files are missing

@MajorBreakfast
Copy link
Contributor

especially around with_context needing two _, _ at the end... any suggestion on a more ergonomic API?

Maybe impl Trait can help

///
/// let mut future = PinBox::new(async {
/// 5
/// });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest

let mut future = async { 5 };
pin_mut!(future);

I think we should prefer the pin_mut! macro in examples because users should reach for it first to avoid the heap allocation

@Nemo157
Copy link
Member Author

Nemo157 commented Aug 5, 2018

Maybe impl Trait can help

Not for this case, since you can't use turbofish when calling the function to fill in the 2 required parameters.

@Nemo157
Copy link
Member Author

Nemo157 commented Aug 5, 2018

😭 features and dev-dependencies seem impossible to work with (I knew there were going to be issues with using futures-test in a no_std crate, but I can't even get it working properly for futures to use it).

@Nemo157
Copy link
Member Author

Nemo157 commented Aug 5, 2018

And that's the entirety of futures/test/support moved over, I think that's probably a good initial cut.

/// #![feature(async_await, futures_api, pin)]
///
/// #[macro_use]
/// extern crate futures_test;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other doc example we write this as:

#![feature(async_await, futures_api, pin)]
#[macro_use] extern crate futures_test;

/// extern crate futures_test;
///
/// #[macro_use]
/// extern crate futures;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this isn't required. We rely on the Rust 2018 module system and there's also no macro from the futures crate in use

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pin_mut! is coming from futures still, this can change to pin_utils or whatever once that's here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right ^^'

/// #[macro_use]
/// extern crate futures;
///
/// # fn main() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main function doesn't need to be explicit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does when using #[macro_use]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, but only when #[macro_use] is on the same line as extern crate...

Copy link
Contributor

@MajorBreakfast MajorBreakfast Aug 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ^^' interesting limitation

/// let mut stream = stream::once((async { 5 }).delay());
/// pin_mut!(stream);
///
/// assert_stream_pending!(stream);

This comment was marked as resolved.

@MajorBreakfast
Copy link
Contributor

MajorBreakfast commented Aug 5, 2018

I'm not so sure about the TestContext API:

  • TestContext is not actually a Context although its name suggests it
  • Its name will often be shorted to "context" which leads to two things being called "context"
  • It is not no_std compatible
  • TestContext::poll works the other way around

Here is a suggestion for an alternative API:

let cx = Context::new(&NoopWaker, &NoopExecutor);
let cx = noop_context(); // Short hand

let cx = Context::new(&NoopWaker, &PanicExecutor);
let cx = no_spawn_context(); // Short hand

let wake_counter = WakeCounter::new();
let cx = noop_context().with_waker(wake_counter.local_waker());
...
wake_counter.count(); // usize

let spawn_recorder = SpawnRecorder::new();
let cx = noop_context().with_executor(spawn_recorder.executor());
...
spawn_recorder.futures(); // Vec<FutureObj<'static, ()>>, later Vec<Box<dyn Future<Output = ()> + Send + 'static>>

Everything except SpawnRecorder is no_std compatible. WakeCounter just needs atomics.

#[macro_use]
extern crate futures_util;

#[macro_export]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[macro_export] above modules is not required

@Nemo157
Copy link
Member Author

Nemo157 commented Aug 5, 2018

The closest I can work out how to do is:

let wake = wake::panic();
let mut executor = executor::Panic;
let cx = Context::new(&wake, &mut executor);

the waker is a LocalWaker, which is non-Sync so can't be a static, so must be created from a function call. Then because the context takes in references the waker and executor both have to be stored in local variables to get a non-temporary lifetime. For SpawnRecorder specifically it would also become much more complicated since it would need to start using thread safe internal mutability.

That's the main reason I wanted a type like TestContext to handle the creation and lifetimes for the waker and executor, with the goal of compressing those three lines into one.

I don't think being no_std compatible is very worthwhile (at least I have not seen any way to actually utilise it in test helpers), it's also basically impossible because of limitations in feature handling with dev-dependencies in Cargo.

@MajorBreakfast
Copy link
Contributor

MajorBreakfast commented Aug 5, 2018

  • I agree that no_std compatibility for testing utilities is not important. Running tests on embedded devices doesn't make much sense. At least not unit tests with dummy wakers and executors.
  • You're right: The lifetimes of my code above don't work out
  • I continue to believe that we shouldn't introduce poll and poll_unpin methods on the TestContext. They are unintuitive because they work the wrong way around and they can't be used with streams and sinks.
  • The code examples should refrain from shortening variables called test_context to context. test_cx is fine though.

And how about introducing:

with_noop_context(|cx| { ... }); // waker and executor are noops
with_no_spawn_context(|cx| { ... }); // waker is a noop, executor panics on spawn attempts
let cx = test_cx.context(); // For use with NLL

@Nemo157
Copy link
Member Author

Nemo157 commented Aug 6, 2018

I continue to believe that we shouldn't introduce poll and poll_unpin methods on the TestContext. They are unintuitive because they work the wrong way around and they can't be used with streams and sinks.

My issue was that using with per call is a lot of semantic overhead that makes the poll hard to read

assert_eq!(Poll::Pending, test_cx.with(|cx| abortable_rx.poll_unpin(cx)));

I think that would be helped a lot with the context method + NLL you suggest, while it's more code it's simpler to read code than what you get with with (although, I'm not sure if NLL adds any extra promotion that would allow this to work without test_cx.context() being saved to a local variable)

assert_eq!(Poll::Pending, abortable_rx.poll_unpin(&mut test_cx.context()));

The code examples should refrain from shortening variables called test_context to context. test_cx is fine though.

👍

with_noop_context(|cx| { ... }); // waker and executor are noops
with_no_spawn_context(|cx| { ... }); // waker is a noop, executor panics on spawn attempts
let cx = test_cx.context(); // For use with NLL

Yep, I realized that a context method should work last night. Didn't have time to fully test it. I'll add these soon.


I'm wondering if there's some lifetime finagling that would make it possible to have

let cx = noop_context();
let cx = panic_context();
let cx = no_spawn_context();

Since those wakers/executors don't actually do anything having multiple mutable references to a static executor should be safe (but I'm going to have to investigate how to make it sound...), LocalWaker on the other hand I'm wondering if there's a way to leverage thread local storage to have static instances.

@MajorBreakfast
Copy link
Contributor

MajorBreakfast commented Aug 6, 2018

@Nemo157 If you could make noop_context() and friends work via thread local storage, that would IMO be truly awesome. It could make this possible:

future.poll(noop_context());

Edit: Depends of course on the return type (if it's a reference or a value) There could also be two functions: One that returns a ref, one that returns a value

let cx = &mut $crate::task::no_spawn_context();
match $crate::futures_core_reexport::stream::Stream::poll_next(
stream, cx,
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using

if poll.is_ready() {}

@@ -1,10 +1,19 @@
//! Task related utilities.
//!
//! For the majority of usecases you can use the functions exported below
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the majority of use cases

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and "functions" is mentioned twice in this sentence

///
/// let mut cx = panic_context();
/// let mut executor = executor::Noop::new();
/// let cx = &mut cx.with_executor(&mut executor);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues with Context's current APIs here:

The first (that's repeated in most of the examples) is that with_executor and with_waker take &mut self, that's useful for being able to create a sub-context with a different executor inside a poll function, but it is what makes the first of these three lines necessary. If with_executor instead took self by value we should be able to merge the first and third lines into

let cx = &mut panic_context().with_executor(&mut executor);

Second, with_executor requires E: Sized so we can't use the unsized Noop::executor_mut(), if we removed this bound then we can merge the second and third lines into

let cx = &mut cx.with_executor(executor::Noop::executor_mut());

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to allow taking the context by value while still being able to create a sub-context inside poll could be to add

impl<'a> Context<'a> {
    pub fn reborrow<'b>(&'b mut self) -> Context<'b> {
        Context {
            local_waker: self.local_waker,
            executor: self.executor,
        }
    }

Then the WithExecutor combinator would just have to change

diff --git futures-util/src/future/with_executor.rs futures-util/src/future/with_executor.rs
index 72b7322e8..f283b7b5a 100644
--- futures-util/src/future/with_executor.rs
+++ futures-util/src/future/with_executor.rs
@@ -32,6 +32,6 @@ impl<Fut, E> Future for WithExecutor<Fut, E>
         let this = unsafe { PinMut::get_mut_unchecked(self) };
         let fut = unsafe { PinMut::new_unchecked(&mut this.future) };
         let exec = &mut this.executor;
-        fut.poll(&mut cx.with_executor(exec))
+        fut.poll(&mut cx.reborrow().with_executor(exec))
     }
 }

//! to use in your tests.
//!
//! For more complex test cases you can take a `Context` from one of these
//! functions then use the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*and * then

/// ```
/// #![feature(async_await, futures_api, pin)]
///
/// use futures::stream;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our code examples usually have no free line between #![feature] and use statements

@@ -0,0 +1,8 @@
//! Implementations of [`Executor`][futures_core::task::Executor] with various
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename this to executor/mod.rs instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've been getting quite annoyed at the new file scheme breaking tab completion, so will switch this back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They changed it because the old system can lead to loads of mod.rs file tabs in an editor. However, that's IMO fine if these files are just lightweight files that just reexport the right things from their child modules. So, I continue to like mod.rs files 🙂

mod panic;
mod record;

pub use self::{noop::Noop, panic::Panic, record::Record};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the

mod noop;
use self::noop::Noop;

mod panic;
use self::panic::Panic;

// --snip--

style

@Nemo157 Nemo157 force-pushed the testing-utilities branch 3 times, most recently from 23dd60b to 0473cda Compare August 15, 2018 10:38
@MajorBreakfast
Copy link
Contributor

@Nemo157 I changed various things. Can you have a look?

@MajorBreakfast MajorBreakfast merged commit 8fb41ab into rust-lang:master Aug 17, 2018
@Nemo157 Nemo157 deleted the testing-utilities branch February 16, 2019 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants