-
-
Notifications
You must be signed in to change notification settings - Fork 28
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
Simultaneous HTTP downloads #121
Merged
Merged
Changes from 41 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
69a5f05
two clients
podusowski a005d9d
draft of the future handling structure
podusowski 28208e7
handle all cases
podusowski 7845326
do not loose ongoing downloads
podusowski 5a8c149
put some login into a function
podusowski f6fa01d
reuse client
podusowski 8fa933f
warning removed
podusowski 303253b
comment removed
podusowski ed2d3b7
minor change
podusowski aaded03
extract fn
podusowski 277902d
draft
podusowski 443c744
http mock skeleton
podusowski cf5e9c1
Mock struct
podusowski dcba219
more work on server
podusowski 11dca02
use MockRequest
podusowski 5634049
pass state around
podusowski 780e679
super basic case works
podusowski 8d08338
doc
podusowski bfd1f41
handle swapped cases
podusowski 5579c4b
random port
podusowski 317d392
add logger
podusowski d045dba
deadlock fixed
podusowski 5bd66fc
bind fn
podusowski a6a1a9e
int test
podusowski 16bac7d
logo - DallE powered
podusowski 6b691a0
warnings removed
podusowski 219ab42
simplify
podusowski 833611e
record unexpected
podusowski 50aebcf
dont return Result
podusowski 592fa62
minor
podusowski cfc62e8
doc
podusowski 1cc4c86
reexport hyper Bytes
podusowski 67c7839
api rework
podusowski e8fccf2
test for simultaneous downloads
podusowski a419af7
test renamed
podusowski 07c2d24
couple renames
podusowski a2d981c
couple renames
podusowski 911c9cb
rename
podusowski a061af9
port is now fn
podusowski aa84687
test renamed
podusowski 6322911
can not anticipate twice
podusowski 9bdd23f
Merge branch 'main' into multi-download
podusowski 3afb416
HttpOptions does not need to be Clone
podusowski bfb0f51
increase limit
podusowski 5a2dde9
test renamed
podusowski 0e1e00b
warnings removed
podusowski 35f156b
changelog updated
podusowski 1c1cdcb
Downloads enum
podusowski 8c068f5
cleanup
podusowski 533584e
single assign
podusowski fdfb660
misc
podusowski d038966
rename
podusowski 803a5c0
move logic to new fn
podusowski 8979920
inline
podusowski 3b3ee0a
rename
podusowski 7bfe171
another rename
podusowski 94937ef
inline + comments
podusowski 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
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,17 @@ | ||
[package] | ||
name = "hypermocker" | ||
edition = "2021" | ||
version = "0.1.0" | ||
publish = false | ||
|
||
[dependencies] | ||
hyper = { version = "1.1.0", features = ["full"] } | ||
tokio = { version = "1.28", features = ["macros"] } | ||
hyper-util = { version = "0.1", features = ["full"] } | ||
http-body-util = "0.1" | ||
log = "0.4" | ||
|
||
[dev-dependencies] | ||
reqwest = "0.11" | ||
futures = "0.3.28" | ||
env_logger = "0.10" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,169 @@ | ||
use http_body_util::Full; | ||
use hyper::{server::conn::http1, Response}; | ||
use hyper_util::rt::TokioIo; | ||
use std::{ | ||
collections::HashMap, | ||
future::Future, | ||
net::SocketAddr, | ||
pin::Pin, | ||
sync::{Arc, Mutex}, | ||
}; | ||
use tokio::net::TcpListener; | ||
use tokio::sync::oneshot; | ||
|
||
pub use hyper::body::Bytes; | ||
|
||
struct Expectation { | ||
payload_rx: oneshot::Receiver<Bytes>, | ||
happened_tx: oneshot::Sender<()>, | ||
} | ||
|
||
#[derive(Default)] | ||
struct State { | ||
/// Anticipations made by [`Mock::anticipate`]. | ||
expectations: HashMap<String, Expectation>, | ||
|
||
/// Requests that were unexpected. | ||
unexpected: Vec<String>, | ||
} | ||
|
||
pub struct Server { | ||
port: u16, | ||
state: Arc<Mutex<State>>, | ||
} | ||
|
||
impl Server { | ||
/// Create new [`Mock`], and bind it to a random port. | ||
pub async fn bind() -> Server { | ||
let state = Arc::new(Mutex::new(State::default())); | ||
|
||
let addr = SocketAddr::from(([127, 0, 0, 1], 0)); | ||
let listener = TcpListener::bind(addr).await.unwrap(); | ||
let port = listener.local_addr().unwrap().port(); | ||
|
||
let state_clone = state.clone(); | ||
tokio::spawn(async move { | ||
loop { | ||
let (stream, _) = listener.accept().await.unwrap(); | ||
let io = TokioIo::new(stream); | ||
|
||
let state = state_clone.clone(); | ||
tokio::task::spawn(async move { | ||
http1::Builder::new() | ||
.serve_connection(io, Service { state }) | ||
.await | ||
.unwrap(); | ||
}); | ||
} | ||
}); | ||
|
||
Server { port, state } | ||
} | ||
|
||
/// Port, which this server listens on. | ||
pub fn port(&self) -> u16 { | ||
self.port | ||
} | ||
|
||
/// Anticipate a HTTP request, but do not respond to it yet. | ||
pub async fn anticipate(&self, url: String) -> AnticipatedRequest { | ||
log::info!("Anticipating '{}'.", url); | ||
let (payload_tx, payload_rx) = oneshot::channel(); | ||
let (happened_tx, happened_rx) = oneshot::channel(); | ||
if self | ||
.state | ||
.lock() | ||
.unwrap() | ||
.expectations | ||
.insert( | ||
url, | ||
Expectation { | ||
payload_rx, | ||
happened_tx, | ||
}, | ||
) | ||
.is_some() | ||
{ | ||
panic!("already anticipating"); | ||
}; | ||
AnticipatedRequest { | ||
payload_tx, | ||
happened_rx: Some(happened_rx), | ||
} | ||
} | ||
} | ||
|
||
impl Drop for Server { | ||
fn drop(&mut self) { | ||
if !self.state.lock().unwrap().unexpected.is_empty() { | ||
panic!("there are unexpected requests"); | ||
} | ||
} | ||
} | ||
|
||
/// HTTP request that was anticipated to arrive. | ||
pub struct AnticipatedRequest { | ||
payload_tx: tokio::sync::oneshot::Sender<Bytes>, | ||
happened_rx: Option<oneshot::Receiver<()>>, | ||
} | ||
|
||
impl AnticipatedRequest { | ||
/// Respond to this request with the given body. | ||
pub async fn respond(self, payload: Bytes) { | ||
log::info!("Responding."); | ||
self.payload_tx.send(payload).unwrap(); | ||
} | ||
|
||
/// Expect the request to come, but still do not respond to it yet. | ||
pub async fn expect(&mut self) { | ||
if let Some(happened_tx) = self.happened_rx.take() { | ||
happened_tx.await.unwrap(); | ||
} else { | ||
panic!("this request was already expected"); | ||
} | ||
} | ||
} | ||
|
||
struct Service { | ||
state: Arc<Mutex<State>>, | ||
} | ||
|
||
impl hyper::service::Service<hyper::Request<hyper::body::Incoming>> for Service { | ||
type Response = Response<Full<Bytes>>; | ||
type Error = hyper::Error; | ||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; | ||
|
||
fn call(&self, request: hyper::Request<hyper::body::Incoming>) -> Self::Future { | ||
log::info!("Incoming request '{}'.", request.uri()); | ||
let state = self.state.clone(); | ||
Box::pin(async move { | ||
let expectation = state | ||
.lock() | ||
.unwrap() | ||
.expectations | ||
.remove(&request.uri().path().to_string()); | ||
|
||
if let Some(expectation) = expectation { | ||
log::debug!("Responding."); | ||
|
||
// [`AnticipatedRequest`] might be dropped by now, and there is no one to receive it, | ||
// but that is OK. | ||
let _ = expectation.happened_tx.send(()); | ||
|
||
let payload = expectation.payload_rx.await.unwrap(); | ||
Ok(Response::new(Full::new(payload))) | ||
} else { | ||
log::warn!("Unexpected '{}'.", request.uri()); | ||
state | ||
.lock() | ||
.unwrap() | ||
.unexpected | ||
.push(request.uri().to_string()); | ||
Ok(Response::builder() | ||
.status(418) | ||
.body(Full::new(Bytes::from_static(b"unexpected"))) | ||
.unwrap()) | ||
} | ||
}) | ||
} | ||
} |
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,76 @@ | ||
use hyper::body::Bytes; | ||
use hypermocker::Server; | ||
use std::time::Duration; | ||
|
||
#[tokio::test] | ||
async fn anticipate_then_request() { | ||
let _ = env_logger::try_init(); | ||
|
||
let mock = Server::bind().await; | ||
let url = format!("http://localhost:{}/foo", mock.port()); | ||
let request = mock.anticipate("/foo".to_string()).await; | ||
|
||
// Make sure that mock's internals kick in. | ||
tokio::time::sleep(Duration::from_secs(1)).await; | ||
|
||
futures::future::join( | ||
async { | ||
let response = reqwest::get(url).await.unwrap(); | ||
let bytes = response.bytes().await.unwrap(); | ||
assert_eq!(&bytes[..], b"hello"); | ||
}, | ||
async { | ||
request.respond(Bytes::from_static(b"hello")).await; | ||
}, | ||
) | ||
.await; | ||
} | ||
|
||
#[tokio::test] | ||
async fn anticipate_expect_then_request() { | ||
let _ = env_logger::try_init(); | ||
|
||
let mock = Server::bind().await; | ||
let url = format!("http://localhost:{}/foo", mock.port()); | ||
let mut request = mock.anticipate("/foo".to_string()).await; | ||
|
||
// Make sure that mock's internals kick in. | ||
tokio::time::sleep(Duration::from_secs(1)).await; | ||
|
||
futures::future::join( | ||
async { | ||
let response = reqwest::get(url).await.unwrap(); | ||
let bytes = response.bytes().await.unwrap(); | ||
assert_eq!(&bytes[..], b"hello"); | ||
}, | ||
async { | ||
request.expect().await; | ||
request.respond(Bytes::from_static(b"hello")).await; | ||
}, | ||
) | ||
.await; | ||
} | ||
|
||
#[tokio::test] | ||
#[should_panic(expected = "there are unexpected requests")] | ||
async fn unanticipated_request() { | ||
let _ = env_logger::try_init(); | ||
|
||
let mock = Server::bind().await; | ||
let url = format!("http://localhost:{}/foo", mock.port()); | ||
|
||
let response = reqwest::get(url).await.unwrap(); | ||
let bytes = response.bytes().await.unwrap(); | ||
assert_eq!(&bytes[..], b"unexpected"); | ||
} | ||
|
||
#[tokio::test] | ||
#[should_panic(expected = "already anticipating")] | ||
async fn can_not_anticipate_twice() { | ||
let _ = env_logger::try_init(); | ||
|
||
let mock = Server::bind().await; | ||
|
||
mock.anticipate("/foo".to_string()).await; | ||
mock.anticipate("/foo".to_string()).await; | ||
} |
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
Oops, something went wrong.
Oops, something went wrong.
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.
I want to get rid of Mockito, and use this crate instead. Once it can handle all Walkers' use cases, I will create a new repo for it.