-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
(WIP): Adding Stream mock to tokio-test #4463
Closed
Closed
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
63e5fb3
(WIP): Draft of PR
Grubba27 6864246
Added stream builder
Grubba27 13d740d
started adjusting and addressing comments
Grubba27 fa77e9f
adjusted rust rust lint
Grubba27 e5b9c8a
run cargo clippy
Grubba27 553abd0
started addressing comments
Grubba27 4115fef
adjusted comments
Grubba27 238816d
removed ./idea files
Grubba27 e7ce82e
removed ./idea files
Grubba27 7d8eb26
removed StreamBuilder struct from io
Grubba27 29aee9d
addresed comments
Grubba27 0631ec5
ran rust linter
Grubba27 f9e78fd
adjusted stream read and next
Grubba27 ab39083
started addressing comments
Grubba27 bd3b767
Merge branch 'tokio-rs:master' into issue-4106
Grubba27 24fad5c
fix: adjusted missing returns
Grubba27 b22f664
fix compilation errors
Grubba27 dac639d
Update tokio-test/src/io_stream.rs
Grubba27 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
#![cfg(not(loom))] | ||
|
||
//! A mock type implementing [`poll_next`]. | ||
//! | ||
//! | ||
//! # Overview | ||
//! | ||
//! TODO | ||
//! | ||
//! # Usage | ||
//! | ||
//! Attempting to write data that the mock isn't expecting will result in a | ||
//! panic. | ||
//! | ||
//! [`AsyncRead`]: tokio::io::AsyncRead | ||
//! [`AsyncWrite`]: tokio::io::AsyncWrite | ||
|
||
|
||
use tokio::sync::mpsc; | ||
use tokio::time::{Duration, Instant, Sleep}; | ||
use tokio_stream::wrappers::UnboundedReceiverStream; | ||
use futures_core::{ready, Stream}; | ||
use std::future::Future; | ||
use std::collections::VecDeque; | ||
use std::fmt; | ||
use std::pin::Pin; | ||
use std::sync::Arc; | ||
use std::task::{self, Context, Poll, Waker}; | ||
use std::{cmp, io}; | ||
|
||
/// An I/O object that follows a predefined script. | ||
/// | ||
/// This value is created by `Builder` and implements `AsyncRead` + `AsyncWrite`. It | ||
/// follows the scenario described by the builder and panics otherwise. | ||
#[derive(Debug)] | ||
pub struct Mock { | ||
inner: Inner, | ||
} | ||
|
||
/// A handle to send additional actions to the related `Mock`. | ||
#[derive(Debug)] | ||
pub struct Handle { | ||
tx: mpsc::UnboundedSender<Action>, | ||
} | ||
|
||
/// Builds `Mock` instances. | ||
#[derive(Debug, Clone, Default)] | ||
pub struct StreamBuilder { | ||
// Sequence of actions for the Mock to take | ||
actions: VecDeque<Action>, | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
enum Action { | ||
Read(Vec<u8>), | ||
Write(Vec<u8>), | ||
Wait(Duration), | ||
Next(String), | ||
// Wrapped in Arc so that Builder can be cloned and Send. | ||
// Mock is not cloned as does not need to check Rc for ref counts. | ||
ReadError(Option<Arc<io::Error>>), | ||
WriteError(Option<Arc<io::Error>>), | ||
} | ||
|
||
struct Inner { | ||
actions: VecDeque<Action>, | ||
waiting: Option<Instant>, | ||
sleep: Option<Pin<Box<Sleep>>>, | ||
read_wait: Option<Waker>, | ||
rx: UnboundedReceiverStream<Action>, | ||
} | ||
|
||
impl StreamBuilder { | ||
/// Return a new, empty `Builder. | ||
pub fn new() -> Self { | ||
Self::default() | ||
} | ||
|
||
/// Sequence a `read` operation. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `read` call | ||
/// and return `buf`. | ||
pub fn read(&mut self, buf: &[u8]) -> &mut Self { | ||
self.actions.push_back(Action::Read(buf.into())); | ||
self | ||
} | ||
|
||
/// Sequence a `read` operation. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `read` call | ||
/// and return `buf`. | ||
pub fn read_stream(&mut self, string: String) -> &mut Self { | ||
self.actions.push_back(Action::Read(string.into())); | ||
self | ||
} | ||
|
||
/// Sequence a `read` operation that produces an error. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `read` call | ||
/// and return `error`. | ||
pub fn read_error(&mut self, error: io::Error) -> &mut Self { | ||
let error = Some(error.into()); | ||
self.actions.push_back(Action::ReadError(error)); | ||
self | ||
} | ||
|
||
/// Sequence a `write` operation. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `write` | ||
/// call. | ||
pub fn write(&mut self, buf: &[u8]) -> &mut Self { | ||
self.actions.push_back(Action::Write(buf.into())); | ||
self | ||
} | ||
|
||
/// Sequence a `write` operation that produces an error. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `write` | ||
/// call that provides `error`. | ||
pub fn write_error(&mut self, error: io::Error) -> &mut Self { | ||
let error = Some(error.into()); | ||
self.actions.push_back(Action::WriteError(error)); | ||
self | ||
} | ||
|
||
/// Sequence a wait. | ||
/// | ||
/// The next operation in the mock's script will be to wait without doing so | ||
/// for `duration` amount of time. | ||
pub fn wait(&mut self, duration: Duration) -> &mut Self { | ||
let duration = cmp::max(duration, Duration::from_millis(1)); | ||
self.actions.push_back(Action::Wait(duration)); | ||
self | ||
} | ||
/// calls next value within stream. | ||
/// | ||
pub fn next(&mut self) -> &mut Self { | ||
self.actions.pop_front(); | ||
self | ||
} | ||
|
||
/// Build a `Mock` value according to the defined script. | ||
pub fn build(&mut self) -> Mock { | ||
let (mock, _) = self.build_with_handle(); | ||
mock | ||
} | ||
|
||
/// Build a `Mock` value paired with a handle | ||
pub fn build_with_handle(&mut self) -> (Mock, Handle) { | ||
let (inner, handle) = Inner::new(self.actions.clone()); | ||
|
||
let mock = Mock { inner }; | ||
|
||
(mock, handle) | ||
} | ||
} | ||
|
||
|
||
|
||
impl Handle { | ||
/// Sequence a `read` operation. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `read` call | ||
/// and return `buf`. | ||
pub fn read(&mut self, buf: &[u8]) -> &mut Self { | ||
self.tx.send(Action::Read(buf.into())).unwrap(); | ||
self | ||
} | ||
|
||
/// Sequence a `read` operation error. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `read` call | ||
/// and return `error`. | ||
pub fn read_error(&mut self, error: io::Error) -> &mut Self { | ||
let error = Some(error.into()); | ||
self.tx.send(Action::ReadError(error)).unwrap(); | ||
self | ||
} | ||
|
||
/// Sequence a `write` operation. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `write` | ||
/// call. | ||
pub fn write(&mut self, buf: &[u8]) -> &mut Self { | ||
self.tx.send(Action::Write(buf.into())).unwrap(); | ||
self | ||
} | ||
|
||
/// Sequence a `write` operation error. | ||
/// | ||
/// The next operation in the mock's script will be to expect a `write` | ||
/// call error. | ||
pub fn write_error(&mut self, error: io::Error) -> &mut Self { | ||
let error = Some(error.into()); | ||
self.tx.send(Action::WriteError(error)).unwrap(); | ||
self | ||
} | ||
} | ||
|
||
impl Inner { | ||
fn new(actions: VecDeque<Action>) -> (Inner, Handle) { | ||
let (tx, rx) = mpsc::unbounded_channel(); | ||
|
||
let rx = UnboundedReceiverStream::new(rx); | ||
|
||
let inner = Inner { | ||
actions, | ||
sleep: None, | ||
read_wait: None, | ||
rx, | ||
waiting: None, | ||
}; | ||
|
||
let handle = Handle { tx }; | ||
|
||
(inner, handle) | ||
} | ||
|
||
fn poll_action(&mut self, cx: &mut task::Context<'_>) -> Poll<Option<Action>> { | ||
Pin::new(&mut self.rx).poll_next(cx) | ||
} | ||
|
||
} | ||
|
||
// ===== impl Inner ===== | ||
|
||
|
||
impl Stream for Mock { | ||
type Item = String; | ||
|
||
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { | ||
|
||
loop { | ||
if let Some(ref mut sleep) = self.inner.sleep { | ||
ready!(Pin::new(sleep).poll(_cx)); | ||
} | ||
|
||
// If a sleep is set, it has already fired | ||
self.inner.sleep = None; | ||
match ready!(self.inner.poll_action(_cx)) { | ||
None => { | ||
return Poll::Pending; | ||
} | ||
Some(action) => { | ||
self.inner.actions.push_back(action); | ||
continue; | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
} | ||
|
||
/// Ensures that Mock isn't dropped with data "inside". | ||
impl Drop for Mock { | ||
fn drop(&mut self) { | ||
// Avoid double panicking, since makes debugging much harder. | ||
if std::thread::panicking() { | ||
return; | ||
} | ||
|
||
self.inner.actions.iter().for_each(|a| match a { | ||
Action::Read(data) => assert!(data.is_empty(), "There is still data left to read."), | ||
Action::Write(data) => assert!(data.is_empty(), "There is still data left to write."), | ||
_ => (), | ||
}) | ||
} | ||
} | ||
|
||
impl fmt::Debug for Inner { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "Inner {{...}}") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
poll_action
returnsNone
, then isn't that because we are at the end and the stream is complete? Then you should returnPoll::Ready(None)
instead ofPoll::Pending
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay now I see what I was doing wrong!!
I don't have any ideia why my mock.next.await is aways returning None for me. perhaps because of the cx ?
Maybe I should use a mock Context too for using pool_next(cx) ? or keep with next() but change the way that I call Action it at io_steram ?