diff --git a/core/lib/src/local/asynchronous/client.rs b/core/lib/src/local/asynchronous/client.rs index 730381a4..ca3f9b52 100644 --- a/core/lib/src/local/asynchronous/client.rs +++ b/core/lib/src/local/asynchronous/client.rs @@ -6,10 +6,45 @@ use crate::rocket::{Rocket, Cargo}; use crate::http::{Method, private::CookieJar}; use crate::error::LaunchError; +struct_client! { [ +/// +/// ### Synchronization +/// +/// While `Client` implements `Sync`, using it in a multithreaded environment +/// while tracking cookies can result in surprising, non-deterministic behavior. +/// This is because while cookie modifications are serialized, the exact +/// ordering depends on when requests are dispatched. Specifically, when cookie +/// tracking is enabled, all request dispatches are serialized, which in-turn +/// serializes modifications to the internally tracked cookies. +/// +/// If possible, refrain from sharing a single instance of `Client` across +/// multiple threads. Instead, prefer to create a unique instance of `Client` +/// per thread. If it's not possible, ensure that either you are not depending +/// on cookies, the ordering of their modifications, or both, or have arranged +/// for dispatches to occur in a deterministic ordering. +/// +/// ## Example +/// +/// The following snippet creates a `Client` from a `Rocket` instance and +/// dispatches a local request to `POST /` with a body of `Hello, world!`. +/// +/// ```rust +/// use rocket::local::asynchronous::Client; +/// +/// # rocket::async_test(async { +/// let rocket = rocket::ignite(); +/// let client = Client::new(rocket).await.expect("valid rocket"); +/// let response = client.post("/") +/// .body("Hello, world!") +/// .dispatch().await; +/// # }); +/// ``` +] pub struct Client { cargo: Cargo, pub(crate) cookies: Option>, } +} impl Client { pub(crate) async fn _new( diff --git a/core/lib/src/local/asynchronous/mod.rs b/core/lib/src/local/asynchronous/mod.rs index 0fa4802c..ddb737fe 100644 --- a/core/lib/src/local/asynchronous/mod.rs +++ b/core/lib/src/local/asynchronous/mod.rs @@ -1,3 +1,9 @@ +//! Structures for asynchronous local dispatching of requests, primarily for +//! testing. +//! +//! This module contains the `asynchronous` variant of the `local` API: it can +//! be used with `#[rocket::async_test]` or another asynchronous test harness. + mod client; mod request; mod response; diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index 7ff4165a..ed06f640 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -5,12 +5,33 @@ use crate::http::{Status, Method, uri::Origin, ext::IntoOwned}; use super::{Client, LocalResponse}; +struct_request! { [ +/// ## Example +/// +/// The following snippet uses the available builder methods to construct a +/// `POST` request to `/` with a JSON body: +/// +/// ```rust +/// use rocket::local::asynchronous::{Client, LocalRequest}; +/// use rocket::http::{ContentType, Cookie}; +/// +/// # rocket::async_test(async { +/// let client = Client::new(rocket::ignite()).await.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 }"#); +/// # }); +/// ``` +] pub struct LocalRequest<'c> { client: &'c Client, request: Request<'c>, data: Vec, uri: Cow<'c, str>, } +} impl<'c> LocalRequest<'c> { pub(crate) fn new( diff --git a/core/lib/src/local/asynchronous/response.rs b/core/lib/src/local/asynchronous/response.rs index 35085957..22b66c0b 100644 --- a/core/lib/src/local/asynchronous/response.rs +++ b/core/lib/src/local/asynchronous/response.rs @@ -1,9 +1,11 @@ use crate::{Request, Response}; +struct_response! { pub struct LocalResponse<'c> { pub(in super) _request: Request<'c>, pub(in super) inner: Response<'c>, } +} impl<'c> LocalResponse<'c> { fn _response(&self) -> &Response<'c> { diff --git a/core/lib/src/local/blocking/client.rs b/core/lib/src/local/blocking/client.rs index 9ef4e4af..7c296066 100644 --- a/core/lib/src/local/blocking/client.rs +++ b/core/lib/src/local/blocking/client.rs @@ -5,10 +5,28 @@ use crate::http::Method; use crate::local::{asynchronous, blocking::LocalRequest}; use crate::rocket::{Rocket, Cargo}; +struct_client! { [ +/// +/// ## Example +/// +/// The following snippet creates a `Client` from a `Rocket` instance and +/// dispatches a local request to `POST /` with a body of `Hello, world!`. +/// +/// ```rust +/// use rocket::local::blocking::Client; +/// +/// let rocket = rocket::ignite(); +/// let client = Client::new(rocket).expect("valid rocket"); +/// let response = client.post("/") +/// .body("Hello, world!") +/// .dispatch(); +/// ``` +] pub struct Client { pub(crate) inner: asynchronous::Client, runtime: RefCell, } +} impl Client { fn _new(rocket: Rocket, tracked: bool) -> Result { diff --git a/core/lib/src/local/blocking/mod.rs b/core/lib/src/local/blocking/mod.rs index 2e04c128..3f78c47c 100644 --- a/core/lib/src/local/blocking/mod.rs +++ b/core/lib/src/local/blocking/mod.rs @@ -1,103 +1,9 @@ -// TODO: Explain difference from async Client +//! Structures for blocking local dispatching of requests, primarily for +//! testing. //! -//! 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.into_string(), Some("Hello, world!".into())); -//! } -//! } -//! ``` -//! -//! [`Client`]: crate::local::blocking::Client -//! [`LocalRequest`]: crate::local::blocking::LocalRequest +//! This module contains the `blocking` variant of the `local` API: it can be +//! used in Rust's synchronous `#[test]` harness. This is accomplished by +//! starting and running an interal asynchronous Runtime as needed. mod client; mod request; diff --git a/core/lib/src/local/blocking/request.rs b/core/lib/src/local/blocking/request.rs index 0698be91..c0bf19c5 100644 --- a/core/lib/src/local/blocking/request.rs +++ b/core/lib/src/local/blocking/request.rs @@ -3,11 +3,31 @@ use std::borrow::Cow; use crate::{Request, http::Method, local::asynchronous}; use super::{Client, LocalResponse}; + +struct_request! { [ +/// ## 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, LocalRequest}; +/// 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 }"#); +/// ``` +] #[derive(Clone)] pub struct LocalRequest<'c> { inner: asynchronous::LocalRequest<'c>, client: &'c Client, } +} impl<'c> LocalRequest<'c> { #[inline] diff --git a/core/lib/src/local/blocking/response.rs b/core/lib/src/local/blocking/response.rs index de78f797..79aa82fa 100644 --- a/core/lib/src/local/blocking/response.rs +++ b/core/lib/src/local/blocking/response.rs @@ -2,10 +2,12 @@ use crate::{Response, local::asynchronous}; use super::Client; +struct_response! { pub struct LocalResponse<'c> { pub(in super) inner: asynchronous::LocalResponse<'c>, pub(in super) client: &'c Client, } +} impl<'c> LocalResponse<'c> { fn _response(&self) -> &Response<'c> { diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs index 66003b65..40ea24d1 100644 --- a/core/lib/src/local/client.rs +++ b/core/lib/src/local/client.rs @@ -1,66 +1,40 @@ -//! A structure to construct requests for local dispatching. -//! -//! # Usage -//! -//! A `Client` is constructed via the [`new()`] or [`untracked()`] methods from -//! an already constructed `Rocket` instance. Once a value of `Client` has been -//! constructed, the [`LocalRequest`] constructor methods ([`get()`], [`put()`], -//! [`post()`], and so on) can be used to create a `LocalRequest` for -//! dispatching. -//! -//! See the [top-level documentation](crate::local) for more usage information. -//! -//! ## Cookie Tracking -//! -//! A `Client` constructed using [`new()`] 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 that cookie. 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. In these cases, use the -//! [`untracked()`](Client::untracked()) constructor to create a `Client` that -//! _will not_ track cookies. -//! -//! ### Synchronization -//! -//! While `Client` implements `Sync`, using it in a multithreaded environment -//! while tracking cookies can result in surprising, non-deterministic behavior. -//! This is because while cookie modifications are serialized, the exact -//! ordering depends on when requests are dispatched. Specifically, when cookie -//! tracking is enabled, all request dispatches are serialized, which in-turn -//! serializes modifications to the internally tracked cookies. -//! -//! If possible, refrain from sharing a single instance of `Client` across -//! multiple threads. Instead, prefer to create a unique instance of `Client` -//! per thread. If it's not possible, ensure that either you are not depending -//! on cookies, the ordering of their modifications, or both, or have arranged -//! for dispatches to occur in a deterministic ordering. -//! -//! ## Example -//! -//! The following snippet creates a `Client` from a `Rocket` instance and -//! dispatches a local request to `POST /` with a body of `Hello, world!`. -//! -//! ```rust -//! use rocket::local::asynchronous::Client; -//! -//! # rocket::async_test(async { -//! let rocket = rocket::ignite(); -//! let client = Client::new(rocket).await.expect("valid rocket"); -//! let response = client.post("/") -//! .body("Hello, world!") -//! .dispatch().await; -//! # }); -//! ``` -//! -//! [`new()`]: #method.new -//! [`untracked()`]: #method.untracked -//! [`get()`]: #method.get -//! [`put()`]: #method.put -//! [`post()`]: #method.post +macro_rules! struct_client { + ([$(#[$attr:meta])*] $item:item) => +{ + /// A structure to construct requests for local dispatching. + /// + /// # Usage + /// + /// A `Client` is constructed via the [`new()`] or [`untracked()`] methods from + /// an already constructed `Rocket` instance. Once a value of `Client` has been + /// constructed, the [`LocalRequest`] constructor methods ([`get()`], [`put()`], + /// [`post()`], and so on) can be used to create a `LocalRequest` for + /// dispatching. + /// + /// See the [top-level documentation](crate::local) for more usage information. + /// + /// ## Cookie Tracking + /// + /// A `Client` constructed using [`new()`] 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 that cookie. 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. In these cases, use the + /// [`untracked()`](Client::untracked()) constructor to create a `Client` that + /// _will not_ track cookies. + $(#[$attr])* + /// + /// [`new()`]: #method.new + /// [`untracked()`]: #method.untracked + /// [`get()`]: #method.get + /// [`put()`]: #method.put + /// [`post()`]: #method.post + $item +}} macro_rules! req_method { ($import:literal, $NAME:literal, $f:ident, $method:expr) => ( diff --git a/core/lib/src/local/mod.rs b/core/lib/src/local/mod.rs index d1bc193d..4a71e4bf 100644 --- a/core/lib/src/local/mod.rs +++ b/core/lib/src/local/mod.rs @@ -7,7 +7,14 @@ //! //! # Usage //! -//! This module contains a [`Client`] structure that is used to create +//! This module contains two variants of the local API: [`asynchronous`] and +//! [`blocking`]. The primary difference between the two is in usage: the +//! `asynchronous` API requires an asynchronous test entry point such as +//! `#[rocket::async_test]`, while the `blocking` API can be used with +//! `#[test]`. Additionally, several methods in the `asynchronous` API are +//! `async` and must therefore be `await`ed. +//! +//! Both APIs include a [`Client`] structure that is used to create //! [`LocalRequest`] structures that can be dispatched against a given //! [`Rocket`](crate::Rocket) instance. Usage is straightforward: //! @@ -104,7 +111,7 @@ //! ``` //! //! [`Client`]: crate::local::asynchronous::Client -//! [`LocalRequest`]: crate::local::LocalRequest +//! [`LocalRequest`]: crate::local::asynchronous::LocalRequest #[macro_use] mod client; #[macro_use] mod request; diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index 65766d89..7016e00f 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -1,62 +1,38 @@ -//! 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::asynchronous::Client; -//! use rocket::http::{ContentType, Cookie}; -//! -//! # rocket::async_test(async { -//! let client = Client::new(rocket::ignite()).await.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::asynchronous::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 +macro_rules! struct_request { + ([$(#[$attr:meta])*] $item:item) => +{ + /// 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. + /// + $(#[$attr])* + /// + /// # Dispatching + /// + /// A `LocalRequest` is dispatched by calling [`dispatch`]. + /// The `LocalRequest` is consumed and a response is returned. + /// + /// 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`]: super::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 + $item +}} macro_rules! impl_request { ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => diff --git a/core/lib/src/local/response.rs b/core/lib/src/local/response.rs index c89bf47a..1f81ef2b 100644 --- a/core/lib/src/local/response.rs +++ b/core/lib/src/local/response.rs @@ -1,10 +1,17 @@ -//! 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`. +macro_rules! struct_response { + ($item:item) => +{ + /// 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`] + /// implementation with a target of `Response`. In other words, when + /// invoking methods, a `LocalResponse` can be treated exactly as if it were + /// a (read-only) `Response`. + /// + /// [`Deref`]: std::ops::Deref + $item +}} macro_rules! impl_response { ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) =>