From b0328d07e0f06613f538609f2304208bb0fb5149 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 22 Jun 2020 04:54:34 -0700 Subject: [PATCH 01/18] Add a blocking variant of Client, LocalRequest, LocalResponse. Additionally use it in the form_kitchen_sink example and benches. --- core/lib/Cargo.toml | 2 +- core/lib/benches/format-routing.rs | 2 +- core/lib/benches/ranked-routing.rs | 2 +- core/lib/benches/simple-routing.rs | 2 +- core/lib/src/local/blocking/client.rs | 292 ++++++++++++++++++ core/lib/src/local/blocking/mod.rs | 106 +++++++ core/lib/src/local/blocking/request.rs | 380 ++++++++++++++++++++++++ core/lib/src/local/client.rs | 3 +- core/lib/src/local/mod.rs | 1 + examples/form_kitchen_sink/src/tests.rs | 30 +- 10 files changed, 800 insertions(+), 20 deletions(-) create mode 100644 core/lib/src/local/blocking/client.rs create mode 100644 core/lib/src/local/blocking/mod.rs create mode 100644 core/lib/src/local/blocking/request.rs diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 955d450cb8..01ac5429c2 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -28,7 +28,7 @@ ctrl_c_shutdown = ["tokio/signal"] rocket_codegen = { version = "0.5.0-dev", path = "../codegen" } rocket_http = { version = "0.5.0-dev", path = "../http" } futures = "0.3.0" -tokio = { version = "0.2.9", features = ["fs", "io-std", "io-util", "rt-threaded", "sync"] } +tokio = { version = "0.2.19", features = ["fs", "io-std", "io-util", "rt-threaded", "sync"] } yansi = "0.5" log = { version = "0.4", features = ["std"] } toml = "0.4.7" diff --git a/core/lib/benches/format-routing.rs b/core/lib/benches/format-routing.rs index 808ad642dc..59ca442cd8 100644 --- a/core/lib/benches/format-routing.rs +++ b/core/lib/benches/format-routing.rs @@ -22,7 +22,7 @@ mod benches { use super::rocket; use self::test::Bencher; - use rocket::local::Client; + use rocket::local::blocking::Client; use rocket::http::{Accept, ContentType}; fn client(_rocket: rocket::Rocket) -> Option { diff --git a/core/lib/benches/ranked-routing.rs b/core/lib/benches/ranked-routing.rs index aba5e4c437..df1c1933dc 100644 --- a/core/lib/benches/ranked-routing.rs +++ b/core/lib/benches/ranked-routing.rs @@ -36,7 +36,7 @@ mod benches { use super::rocket; use self::test::Bencher; - use rocket::local::Client; + use rocket::local::blocking::Client; use rocket::http::{Accept, ContentType}; fn client(_rocket: rocket::Rocket) -> Option { diff --git a/core/lib/benches/simple-routing.rs b/core/lib/benches/simple-routing.rs index 576f2d5084..b58b2f69b6 100644 --- a/core/lib/benches/simple-routing.rs +++ b/core/lib/benches/simple-routing.rs @@ -48,7 +48,7 @@ mod benches { use super::{hello_world_rocket, rocket}; use self::test::Bencher; - use rocket::local::Client; + use rocket::local::blocking::Client; fn client(_rocket: rocket::Rocket) -> Option { unimplemented!("waiting for sync-client"); diff --git a/core/lib/src/local/blocking/client.rs b/core/lib/src/local/blocking/client.rs new file mode 100644 index 0000000000..6445712a1e --- /dev/null +++ b/core/lib/src/local/blocking/client.rs @@ -0,0 +1,292 @@ +use std::borrow::Cow; +use std::cell::RefCell; + +use crate::error::LaunchError; +use crate::http::Method; +use crate::local::blocking::LocalRequest; +use crate::rocket::{Rocket, Manifest}; + +pub struct Client { + pub(crate) inner: crate::local::Client, + runtime: RefCell, +} + +impl Client { + fn _new(rocket: Rocket, tracked: bool) -> Result { + let mut runtime = tokio::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .expect("create tokio runtime"); + + // Initialize the Rocket instance + let inner = runtime.block_on(crate::local::Client::_new(rocket, tracked))?; + + Ok(Self { inner, runtime: RefCell::new(runtime) }) + } + + pub(crate) fn block_on(&self, fut: F) -> R + where + F: std::future::Future, + { + self.runtime.borrow_mut().block_on(fut) + } + + /// Construct a new `Client` from an instance of `Rocket` with cookie + /// tracking. + /// + /// # Cookie Tracking + /// + /// By default, a `Client` propagates cookie changes made by responses to + /// previously dispatched requests. In other words, if a previously + /// dispatched request resulted in a response that adds a cookie, any future + /// requests will contain the new cookies. Similarly, cookies removed by a + /// response won't be propagated further. + /// + /// This is typically the desired mode of operation for a `Client` as it + /// removes the burden of manually tracking cookies. Under some + /// circumstances, however, disabling this tracking may be desired. The + /// [`untracked()`](Client::untracked()) method creates a `Client` that + /// _will not_ track cookies. + /// + /// # Errors + /// + /// If launching the `Rocket` instance would fail, excepting network errors, + /// the `LaunchError` is returned. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// ``` + #[inline(always)] + pub fn new(rocket: Rocket) -> Result { + Self::_new(rocket, true) + } + + /// Construct a new `Client` from an instance of `Rocket` _without_ cookie + /// tracking. + /// + /// # Cookie Tracking + /// + /// Unlike the [`new()`](Client::new()) constructor, a `Client` returned + /// from this method _does not_ automatically propagate cookie changes. + /// + /// # Errors + /// + /// If launching the `Rocket` instance would fail, excepting network errors, + /// the `LaunchError` is returned. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::untracked(rocket::ignite()).expect("valid rocket"); + /// ``` + #[inline(always)] + pub fn untracked(rocket: Rocket) -> Result { + Self::_new(rocket, false) + } + + /// Returns a reference to the `Manifest` of the `Rocket` this client is + /// creating requests for. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let my_rocket = rocket::ignite(); + /// let client = Client::new(my_rocket).expect("valid rocket"); + /// + /// // get access to the manifest within `client` + /// let manifest = client.manifest(); + /// ``` + #[inline(always)] + pub fn manifest(&self) -> &Manifest { + self.inner.manifest() + } + + /// Create a local `GET` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.get("/hello"); + /// ``` + #[inline(always)] + pub fn get<'c, 'u: 'c, U: Into>>(&'c self, uri: U) -> LocalRequest<'c> { + self.req(Method::Get, uri) + } + + /// Create a local `PUT` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.put("/hello"); + /// ``` + #[inline(always)] + pub fn put<'c, 'u: 'c, U: Into>>(&'c self, uri: U) -> LocalRequest<'c> { + self.req(Method::Put, uri) + } + + /// Create a local `POST` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::ContentType; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// + /// let req = client.post("/hello") + /// .body("field=value&otherField=123") + /// .header(ContentType::Form); + /// ``` + #[inline(always)] + pub fn post<'c, 'u: 'c, U: Into>>(&'c self, uri: U) -> LocalRequest<'c> { + self.req(Method::Post, uri) + } + + /// Create a local `DELETE` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.delete("/hello"); + /// ``` + #[inline(always)] + pub fn delete<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> + where U: Into> + { + self.req(Method::Delete, uri) + } + + /// Create a local `OPTIONS` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.options("/hello"); + /// ``` + #[inline(always)] + pub fn options<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> + where U: Into> + { + self.req(Method::Options, uri) + } + + /// Create a local `HEAD` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.head("/hello"); + /// ``` + #[inline(always)] + pub fn head<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> + where U: Into> + { + self.req(Method::Head, uri) + } + + /// Create a local `PATCH` request to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.patch("/hello"); + /// ``` + #[inline(always)] + pub fn patch<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> + where U: Into> + { + self.req(Method::Patch, uri) + } + + /// Create a local request with method `method` to the URI `uri`. + /// + /// When dispatched, the request will be served by the instance of Rocket + /// within `self`. The request is not dispatched automatically. To actually + /// dispatch the request, call [`LocalRequest::dispatch()`] on the returned + /// request. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::Method; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.req(Method::Get, "/hello"); + /// ``` + #[inline(always)] + pub fn req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c> + where U: Into> + { + LocalRequest::new(self, method, uri.into()) + } +} + +#[cfg(test)] +mod test { + // TODO: assert that client is !Sync - e.g. with static_assertions or another tool +} diff --git a/core/lib/src/local/blocking/mod.rs b/core/lib/src/local/blocking/mod.rs new file mode 100644 index 0000000000..e01ea9d1d0 --- /dev/null +++ b/core/lib/src/local/blocking/mod.rs @@ -0,0 +1,106 @@ +// TODO: Explain difference from async Client +//! +//! Structures for local dispatching of requests, primarily for testing. +//! +//! This module allows for simple request dispatching against a local, +//! non-networked instance of Rocket. The primary use of this module is to unit +//! and integration test Rocket applications by crafting requests, dispatching +//! them, and verifying the response. +//! +//! # Usage +//! +//! This module contains a [`Client`] structure that is used to create +//! [`LocalRequest`] structures that can be dispatched against a given +//! [`Rocket`](crate::Rocket) instance. Usage is straightforward: +//! +//! 1. Construct a `Rocket` instance that represents the application. +//! +//! ```rust +//! let rocket = rocket::ignite(); +//! # let _ = rocket; +//! ``` +//! +//! 2. Construct a `Client` using the `Rocket` instance. +//! +//! ```rust +//! # use rocket::local::blocking::Client; +//! # let rocket = rocket::ignite(); +//! let client = Client::new(rocket).expect("valid rocket instance"); +//! # let _ = client; +//! ``` +//! +//! 3. Construct requests using the `Client` instance. +//! +//! ```rust +//! # use rocket::local::blocking::Client; +//! # let rocket = rocket::ignite(); +//! # let client = Client::new(rocket).unwrap(); +//! let req = client.get("/"); +//! # let _ = req; +//! ``` +//! +//! 3. Dispatch the request to retrieve the response. +//! +//! ```rust +//! # use rocket::local::blocking::Client; +//! # let rocket = rocket::ignite(); +//! # let client = Client::new(rocket).unwrap(); +//! # let req = client.get("/"); +//! let response = req.dispatch(); +//! # let _ = response; +//! ``` +//! +//! All together and in idiomatic fashion, this might look like: +//! +//! ```rust +//! use rocket::local::blocking::Client; +//! +//! let client = Client::new(rocket::ignite()).expect("valid rocket"); +//! let response = client.post("/") +//! .body("Hello, world!") +//! .dispatch(); +//! # let _ = response; +//! ``` +//! +//! # Unit/Integration Testing +//! +//! This module can be used to test a Rocket application by constructing +//! requests via `Client` and validating the resulting response. As an example, +//! consider the following complete "Hello, world!" application, with testing. +//! +//! ```rust +//! #![feature(proc_macro_hygiene)] +//! +//! #[macro_use] extern crate rocket; +//! +//! #[get("/")] +//! fn hello() -> &'static str { +//! "Hello, world!" +//! } +//! +//! # fn main() { } +//! #[cfg(test)] +//! mod test { +//! use super::{rocket, hello}; +//! use rocket::local::blocking::Client; +//! +//! fn test_hello_world() { +//! // Construct a client to use for dispatching requests. +//! let rocket = rocket::ignite().mount("/", routes![hello]); +//! let client = Client::new(rocket).expect("valid rocket instance"); +//! +//! // Dispatch a request to 'GET /' and validate the response. +//! let mut response = client.get("/").dispatch(); +//! assert_eq!(response.body_string(), Some("Hello, world!".into())); +//! } +//! } +//! ``` +//! +//! [`Client`]: crate::local::blocking::Client +//! [`LocalRequest`]: crate::local::blocking::LocalRequest + +mod request; +mod client; + +pub use self::request::{LocalResponse, LocalRequest}; +pub use self::client::Client; diff --git a/core/lib/src/local/blocking/request.rs b/core/lib/src/local/blocking/request.rs new file mode 100644 index 0000000000..dce4cdf040 --- /dev/null +++ b/core/lib/src/local/blocking/request.rs @@ -0,0 +1,380 @@ +use std::fmt; +use std::net::SocketAddr; +use std::ops::{Deref, DerefMut}; +use std::borrow::Cow; + +use crate::{Request, Response}; +use crate::http::{Method, Header, Cookie}; +use crate::local::blocking::Client; + +// TODO: Explain difference from async LocalRequest +/// A structure representing a local request as created by [`Client`]. +/// +/// # Usage +/// +/// A `LocalRequest` value is constructed via method constructors on [`Client`]. +/// Headers can be added via the [`header`] builder method and the +/// [`add_header`] method. Cookies can be added via the [`cookie`] builder +/// method. The remote IP address can be set via the [`remote`] builder method. +/// The body of the request can be set via the [`body`] builder method or +/// [`set_body`] method. +/// +/// ## Example +/// +/// The following snippet uses the available builder methods to construct a +/// `POST` request to `/` with a JSON body: +/// +/// ```rust +/// use rocket::local::blocking::Client; +/// use rocket::http::{ContentType, Cookie}; +/// +/// let client = Client::new(rocket::ignite()).expect("valid rocket"); +/// let req = client.post("/") +/// .header(ContentType::JSON) +/// .remote("127.0.0.1:8000".parse().unwrap()) +/// .cookie(Cookie::new("name", "value")) +/// .body(r#"{ "value": 42 }"#); +/// ``` +/// +/// # Dispatching +/// +/// A `LocalRequest` can be dispatched in one of two ways: +/// +/// 1. [`dispatch`] +/// +/// This method should always be preferred. The `LocalRequest` is consumed +/// and a response is returned. +/// +/// 2. [`mut_dispatch`] +/// +/// This method should _only_ be used when either it is known that the +/// application will not modify the request, or it is desired to see +/// modifications to the request. No cloning occurs, and the request is not +/// consumed. +/// +/// Additionally, note that `LocalRequest` implements `Clone`. As such, if the +/// same request needs to be dispatched multiple times, the request can first be +/// cloned and then dispatched: `request.clone().dispatch()`. +/// +/// [`Client`]: crate::local::blocking::Client +/// [`header`]: #method.header +/// [`add_header`]: #method.add_header +/// [`cookie`]: #method.cookie +/// [`remote`]: #method.remote +/// [`body`]: #method.body +/// [`set_body`]: #method.set_body +/// [`dispatch`]: #method.dispatch +/// [`mut_dispatch`]: #method.mut_dispatch +pub struct LocalRequest<'c> { + inner: crate::local::LocalRequest<'c>, + client: &'c Client, +} + +impl<'c> LocalRequest<'c> { + pub(crate) fn new( + client: &'c Client, + method: Method, + uri: Cow<'c, str> + ) -> LocalRequest<'c> { + let inner = crate::local::LocalRequest::new(&client.inner, method, uri); + Self { inner, client } + } + + /// Retrieves the inner `Request` as seen by Rocket. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).expect("valid rocket"); + /// let req = client.get("/"); + /// let inner_req = req.inner(); + /// ``` + #[inline] + pub fn inner(&self) -> &Request<'c> { + self.inner.inner() + } + + /// Add a header to this request. + /// + /// Any type that implements `Into
` can be used here. Among others, + /// this includes [`ContentType`] and [`Accept`]. + /// + /// [`ContentType`]: crate::http::ContentType + /// [`Accept`]: crate::http::Accept + /// + /// # Examples + /// + /// Add the Content-Type header: + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::ContentType; + /// + /// # #[allow(unused_variables)] + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let req = client.get("/").header(ContentType::JSON); + /// ``` + #[inline] + pub fn header>>(mut self, header: H) -> Self { + self.inner = self.inner.header(header); + self + } + + /// Adds a header to this request without consuming `self`. + /// + /// # Examples + /// + /// Add the Content-Type header: + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::ContentType; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let mut req = client.get("/"); + /// req.add_header(ContentType::JSON); + /// ``` + #[inline] + pub fn add_header>>(&mut self, header: H) { + self.inner.add_header(header) + } + + /// Set the remote address of this request. + /// + /// # Examples + /// + /// Set the remote address to "8.8.8.8:80": + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let address = "8.8.8.8:80".parse().unwrap(); + /// let req = client.get("/").remote(address); + /// ``` + #[inline] + pub fn remote(mut self, address: SocketAddr) -> Self { + self.inner = self.inner.remote(address); + self + } + + /// Add a cookie to this request. + /// + /// # Examples + /// + /// Add `user_id` cookie: + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::Cookie; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// # #[allow(unused_variables)] + /// let req = client.get("/") + /// .cookie(Cookie::new("username", "sb")) + /// .cookie(Cookie::new("user_id", "12")); + /// ``` + #[inline] + pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { + self.inner = self.inner.cookie(cookie); + self + } + + /// Add all of the cookies in `cookies` to this request. + /// + /// # Examples + /// + /// Add `user_id` cookie: + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::Cookie; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let cookies = vec![Cookie::new("a", "b"), Cookie::new("c", "d")]; + /// # #[allow(unused_variables)] + /// let req = client.get("/").cookies(cookies); + /// ``` + #[inline] + pub fn cookies(mut self, cookies: Vec>) -> Self { + self.inner = self.inner.cookies(cookies); + self + } + + /// Add a [private cookie] to this request. + /// + /// This method is only available when the `private-cookies` feature is + /// enabled. + /// + /// [private cookie]: crate::http::Cookies::add_private() + /// + /// # Examples + /// + /// Add `user_id` as a private cookie: + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::Cookie; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// # #[allow(unused_variables)] + /// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb")); + /// ``` + #[inline] + #[cfg(feature = "private-cookies")] + pub fn private_cookie(mut self, cookie: Cookie<'static>) -> Self { + self.inner = self.inner.private_cookie(cookie); + self + } + + // TODO: For CGI, we want to be able to set the body to be stdin without + // actually reading everything into a vector. Can we allow that here while + // keeping the simplicity? Looks like it would require us to reintroduce a + // NetStream::Local(Box) or something like that. + + /// Set the body (data) of the request. + /// + /// # Examples + /// + /// Set the body to be a JSON structure; also sets the Content-Type. + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::ContentType; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// # #[allow(unused_variables)] + /// let req = client.post("/") + /// .header(ContentType::JSON) + /// .body(r#"{ "key": "value", "array": [1, 2, 3], }"#); + /// ``` + #[inline] + pub fn body>(mut self, body: S) -> Self { + self.inner = self.inner.body(body); + self + } + + /// Set the body (data) of the request without consuming `self`. + /// + /// # Examples + /// + /// Set the body to be a JSON structure; also sets the Content-Type. + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// use rocket::http::ContentType; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let mut req = client.post("/").header(ContentType::JSON); + /// req.set_body(r#"{ "key": "value", "array": [1, 2, 3], }"#); + /// ``` + #[inline] + pub fn set_body>(&mut self, body: S) { + self.inner.set_body(body); + } + + /// Dispatches the request, returning the response. + /// + /// This method consumes `self` and is the preferred mechanism for + /// dispatching. + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// let response = client.get("/").dispatch(); + /// ``` + #[inline(always)] + pub fn dispatch(self) -> LocalResponse<'c> { + let inner = self.client.block_on(self.inner.dispatch()); + LocalResponse { inner, client: self.client } + } + + /// Dispatches the request, returning the response. + /// + /// This method _does not_ consume or clone `self`. Any changes to the + /// request that occur during handling will be visible after this method is + /// called. For instance, body data is always consumed after a request is + /// dispatched. As such, only the first call to `mut_dispatch` for a given + /// `LocalRequest` will contains the original body data. + /// + /// This method should _only_ be used when either it is known that + /// the application will not modify the request, or it is desired to see + /// modifications to the request. Prefer to use [`dispatch`] instead. + /// + /// [`dispatch`]: #method.dispatch + /// + /// # Example + /// + /// ```rust + /// use rocket::local::blocking::Client; + /// + /// let client = Client::new(rocket::ignite()).unwrap(); + /// + /// let mut req = client.get("/"); + /// let response_a = req.mut_dispatch(); + /// // TODO.async: Annoying. Is this really a good example to show? + /// drop(response_a); + /// let response_b = req.mut_dispatch(); + /// ``` + #[inline(always)] + pub fn mut_dispatch(&mut self) -> LocalResponse<'c> { + let inner = self.client.block_on(self.inner.mut_dispatch()); + LocalResponse { inner, client: self.client } + } + +} + +impl fmt::Debug for LocalRequest<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.inner.inner(), f) + } +} + +/// A structure representing a response from dispatching a local request. +/// +/// This structure is a thin wrapper around [`Response`]. It implements no +/// methods of its own; all functionality is exposed via the [`Deref`] and +/// [`DerefMut`] implementations with a target of `Response`. In other words, +/// when invoking methods, a `LocalResponse` can be treated exactly as if it +/// were a `Response`. +pub struct LocalResponse<'c> { + inner: crate::local::LocalResponse<'c>, + client: &'c Client, +} + +impl<'c> Deref for LocalResponse<'c> { + type Target = Response<'c>; + + #[inline(always)] + fn deref(&self) -> &Response<'c> { + &*self.inner + } +} + +impl<'c> DerefMut for LocalResponse<'c> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Response<'c> { + &mut *self.inner + } +} + +impl LocalResponse<'_> { + pub fn body_string(&mut self) -> Option { + self.client.block_on(self.inner.body_string()) + } + + pub fn body_bytes(&mut self) -> Option> { + self.client.block_on(self.inner.body_bytes()) + } +} + +impl fmt::Debug for LocalResponse<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&*self.inner, f) + } +} diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index b4570492de..e55badc0bf 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -78,8 +78,9 @@ impl Client { /// Constructs a new `Client`. If `tracked` is `true`, an empty `CookieJar` /// is created for cookie tracking. Otherwise, the internal `CookieJar` is /// set to `None`. - async fn _new(mut rocket: Rocket, tracked: bool) -> Result { + pub(crate) async fn _new(rocket: Rocket, tracked: bool) -> Result { rocket.prelaunch_check().await?; + let cookies = match tracked { true => Some(RwLock::new(CookieJar::new())), false => None diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index 586b3419a4..51cb5e77e7 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -106,6 +106,7 @@ //! [`Client`]: crate::local::Client //! [`LocalRequest`]: crate::local::LocalRequest +pub mod blocking; mod request; mod client; diff --git a/examples/form_kitchen_sink/src/tests.rs b/examples/form_kitchen_sink/src/tests.rs index c6b3f3ba23..5ca88720b0 100644 --- a/examples/form_kitchen_sink/src/tests.rs +++ b/examples/form_kitchen_sink/src/tests.rs @@ -1,7 +1,7 @@ use std::fmt; use super::{rocket, FormInput, FormOption}; -use rocket::local::Client; +use rocket::local::blocking::Client; use rocket::http::ContentType; impl fmt::Display for FormOption { @@ -19,9 +19,9 @@ macro_rules! assert_form_eq { let mut res = $client.post("/") .header(ContentType::Form) .body($form_str) - .dispatch().await; + .dispatch(); - assert_eq!(res.body_string().await, Some($expected)); + assert_eq!(res.body_string(), Some($expected)); }}; } @@ -40,9 +40,9 @@ macro_rules! assert_valid_raw_form { }}; } -#[rocket::async_test] -async fn test_good_forms() { - let client = Client::new(rocket()).await.unwrap(); +#[test] +fn test_good_forms() { + let client = Client::new(rocket()).unwrap(); let mut input = FormInput { checkbox: true, number: 310, @@ -119,9 +119,9 @@ macro_rules! assert_invalid_raw_form { }}; } -#[rocket::async_test] -async fn check_semantically_invalid_forms() { - let client = Client::new(rocket()).await.unwrap(); +#[test] +fn check_semantically_invalid_forms() { + let client = Client::new(rocket()).unwrap(); let mut form_vals = ["true", "1", "a", "hi", "hey", "b"]; form_vals[0] = "not true"; @@ -175,17 +175,17 @@ async fn check_semantically_invalid_forms() { assert_invalid_raw_form!(&client, ""); } -#[rocket::async_test] -async fn check_structurally_invalid_forms() { - let client = Client::new(rocket()).await.unwrap(); +#[test] +fn check_structurally_invalid_forms() { + let client = Client::new(rocket()).unwrap(); assert_invalid_raw_form!(&client, "==&&&&&&=="); assert_invalid_raw_form!(&client, "a&=b"); assert_invalid_raw_form!(&client, "="); } -#[rocket::async_test] -async fn check_bad_utf8() { - let client = Client::new(rocket()).await.unwrap(); +#[test] +fn check_bad_utf8() { + let client = Client::new(rocket()).unwrap(); unsafe { let bad_str = std::str::from_utf8_unchecked(b"a=\xff"); assert_form_eq!(&client, bad_str, "Form input was invalid UTF-8.".into()); From fdb7a6368644607c5366c8767c0bad985596d5f1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 17 Jun 2020 16:40:12 -0700 Subject: [PATCH 02/18] Deduplicate 'Client' code. Test for non-Sync, non-Send. --- contrib/lib/src/templates/mod.rs | 2 +- .../lib/tests/compress_responder.rs.disabled | 2 +- .../lib/tests/compression_fairing.rs.disabled | 2 +- contrib/lib/tests/helmet.rs | 2 +- contrib/lib/tests/static_files.rs | 2 +- contrib/lib/tests/templates.rs | 6 +- core/codegen/tests/expansion.rs | 2 +- core/codegen/tests/responder.rs | 2 +- core/codegen/tests/route-data.rs | 2 +- core/codegen/tests/route-format.rs | 2 +- core/codegen/tests/route-ranking.rs | 2 +- core/codegen/tests/route.rs | 2 +- core/lib/Cargo.toml | 2 +- core/lib/src/local/asynchronous/client.rs | 64 ++ core/lib/src/local/asynchronous/mod.rs | 5 + .../src/local/{ => asynchronous}/request.rs | 121 ++-- core/lib/src/local/blocking/client.rs | 285 ++------- core/lib/src/local/blocking/request.rs | 42 +- core/lib/src/local/client.rs | 567 ++++++------------ core/lib/src/local/mod.rs | 86 ++- core/lib/src/response/status.rs | 6 +- core/lib/src/rocket.rs | 1 + .../lib/tests/absolute-uris-okay-issue-443.rs | 2 +- .../conditionally-set-server-header-996.rs | 2 +- core/lib/tests/derive-reexports.rs | 2 +- .../fairing_before_head_strip-issue-546.rs | 2 +- .../lib/tests/flash-lazy-removes-issue-466.rs | 2 +- core/lib/tests/form_method-issue-45.rs | 2 +- .../lib/tests/form_value_decoding-issue-82.rs | 2 +- core/lib/tests/head_handling.rs | 2 +- core/lib/tests/limits.rs | 2 +- .../local-request-content-type-issue-505.rs | 2 +- .../local_request_private_cookie-issue-368.rs | 2 +- core/lib/tests/nested-fairing-attaches.rs | 2 +- .../tests/precise-content-type-matching.rs | 2 +- .../tests/redirect_from_catcher-issue-113.rs | 2 +- core/lib/tests/route_guard.rs | 2 +- core/lib/tests/segments-issues-41-86.rs | 2 +- core/lib/tests/strict_and_lenient_forms.rs | 2 +- .../tests/uri-percent-encoding-issue-808.rs | 2 +- examples/config/tests/common/mod.rs | 2 +- examples/content_types/src/tests.rs | 2 +- examples/cookies/src/tests.rs | 2 +- examples/errors/src/tests.rs | 2 +- examples/fairings/src/tests.rs | 2 +- examples/form_validation/src/tests.rs | 2 +- examples/handlebars_templates/src/tests.rs | 2 +- examples/hello_2018/src/tests.rs | 4 +- examples/hello_person/src/tests.rs | 2 +- examples/hello_world/src/tests.rs | 2 +- examples/json/src/tests.rs | 2 +- examples/managed_queue/src/tests.rs | 2 +- examples/manual_routes/src/tests.rs | 2 +- examples/msgpack/src/tests.rs | 2 +- examples/optional_redirect/src/tests.rs | 2 +- examples/pastebin/src/tests.rs | 2 +- examples/query_params/src/tests.rs | 2 +- examples/ranking/src/tests.rs | 2 +- examples/raw_sqlite/src/tests.rs | 2 +- examples/raw_upload/src/tests.rs | 2 +- examples/redirect/src/tests.rs | 2 +- examples/request_guard/src/main.rs | 2 +- examples/request_local_state/src/tests.rs | 2 +- examples/session/src/tests.rs | 2 +- examples/state/src/tests.rs | 2 +- examples/static_files/src/tests.rs | 2 +- examples/stream/src/tests.rs | 2 +- examples/tera_templates/src/tests.rs | 2 +- examples/testing/src/main.rs | 2 +- examples/tls/src/tests.rs | 2 +- examples/todo/src/tests.rs | 2 +- examples/uuid/src/tests.rs | 2 +- site/guide/8-testing.md | 4 +- 73 files changed, 469 insertions(+), 844 deletions(-) create mode 100644 core/lib/src/local/asynchronous/client.rs create mode 100644 core/lib/src/local/asynchronous/mod.rs rename core/lib/src/local/{ => asynchronous}/request.rs (79%) diff --git a/contrib/lib/src/templates/mod.rs b/contrib/lib/src/templates/mod.rs index 1c0bb98d01..84ded83a5b 100644 --- a/contrib/lib/src/templates/mod.rs +++ b/contrib/lib/src/templates/mod.rs @@ -328,7 +328,7 @@ impl Template { /// use std::collections::HashMap; /// /// use rocket_contrib::templates::Template; - /// use rocket::local::Client; + /// use rocket::local::asynchronous::Client; /// /// fn main() { /// # rocket::async_test(async { diff --git a/contrib/lib/tests/compress_responder.rs.disabled b/contrib/lib/tests/compress_responder.rs.disabled index 2d317ddfdb..224d7335b2 100644 --- a/contrib/lib/tests/compress_responder.rs.disabled +++ b/contrib/lib/tests/compress_responder.rs.disabled @@ -9,7 +9,7 @@ mod compress_responder_tests { use rocket::http::hyper::header::{ContentEncoding, Encoding}; use rocket::http::Status; use rocket::http::{ContentType, Header}; - use rocket::local::Client; + use rocket::local::asynchronous::Client; use rocket::response::{Content, Response}; use rocket_contrib::compression::Compress; diff --git a/contrib/lib/tests/compression_fairing.rs.disabled b/contrib/lib/tests/compression_fairing.rs.disabled index fa09188d79..dcdaa0d993 100644 --- a/contrib/lib/tests/compression_fairing.rs.disabled +++ b/contrib/lib/tests/compression_fairing.rs.disabled @@ -10,7 +10,7 @@ mod compression_fairing_tests { use rocket::http::hyper::header::{ContentEncoding, Encoding}; use rocket::http::Status; use rocket::http::{ContentType, Header}; - use rocket::local::Client; + use rocket::local::asynchronous::Client; use rocket::response::{Content, Response}; use rocket_contrib::compression::Compression; diff --git a/contrib/lib/tests/helmet.rs b/contrib/lib/tests/helmet.rs index b8742f0be6..9c6add1a83 100644 --- a/contrib/lib/tests/helmet.rs +++ b/contrib/lib/tests/helmet.rs @@ -7,7 +7,7 @@ extern crate rocket; #[cfg(feature = "helmet")] mod helmet_tests { use rocket::http::{Status, uri::Uri}; - use rocket::local::{Client, LocalResponse}; + use rocket::local::asynchronous::{Client, LocalResponse}; use rocket_contrib::helmet::*; use time::Duration; diff --git a/contrib/lib/tests/static_files.rs b/contrib/lib/tests/static_files.rs index 5dcf8f5865..4b593ec63e 100644 --- a/contrib/lib/tests/static_files.rs +++ b/contrib/lib/tests/static_files.rs @@ -8,7 +8,7 @@ mod static_tests { use rocket::{self, Rocket, Route}; use rocket_contrib::serve::{StaticFiles, Options}; use rocket::http::Status; - use rocket::local::Client; + use rocket::local::asynchronous::Client; fn static_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/contrib/lib/tests/templates.rs b/contrib/lib/tests/templates.rs index ed51bfeda0..08c568b241 100644 --- a/contrib/lib/tests/templates.rs +++ b/contrib/lib/tests/templates.rs @@ -42,7 +42,7 @@ mod templates_tests { use super::*; use std::collections::HashMap; use rocket::http::Status; - use rocket::local::Client; + use rocket::local::asynchronous::Client; const UNESCAPED_EXPECTED: &'static str = "\nh_start\ntitle: _test_\nh_end\n\n\n