Document new 'local' structures.

This commit is contained in:
Jeb Rosen 2020-06-28 10:52:52 -07:00 committed by Sergio Benitez
parent 03127f4dae
commit 050a2c6461
12 changed files with 204 additions and 230 deletions

View File

@ -6,10 +6,45 @@ use crate::rocket::{Rocket, Cargo};
use crate::http::{Method, private::CookieJar}; use crate::http::{Method, private::CookieJar};
use crate::error::LaunchError; 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 { pub struct Client {
cargo: Cargo, cargo: Cargo,
pub(crate) cookies: Option<RwLock<CookieJar>>, pub(crate) cookies: Option<RwLock<CookieJar>>,
} }
}
impl Client { impl Client {
pub(crate) async fn _new( pub(crate) async fn _new(

View File

@ -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 client;
mod request; mod request;
mod response; mod response;

View File

@ -5,12 +5,33 @@ use crate::http::{Status, Method, uri::Origin, ext::IntoOwned};
use super::{Client, LocalResponse}; 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> { pub struct LocalRequest<'c> {
client: &'c Client, client: &'c Client,
request: Request<'c>, request: Request<'c>,
data: Vec<u8>, data: Vec<u8>,
uri: Cow<'c, str>, uri: Cow<'c, str>,
} }
}
impl<'c> LocalRequest<'c> { impl<'c> LocalRequest<'c> {
pub(crate) fn new( pub(crate) fn new(

View File

@ -1,9 +1,11 @@
use crate::{Request, Response}; use crate::{Request, Response};
struct_response! {
pub struct LocalResponse<'c> { pub struct LocalResponse<'c> {
pub(in super) _request: Request<'c>, pub(in super) _request: Request<'c>,
pub(in super) inner: Response<'c>, pub(in super) inner: Response<'c>,
} }
}
impl<'c> LocalResponse<'c> { impl<'c> LocalResponse<'c> {
fn _response(&self) -> &Response<'c> { fn _response(&self) -> &Response<'c> {

View File

@ -5,10 +5,28 @@ use crate::http::Method;
use crate::local::{asynchronous, blocking::LocalRequest}; use crate::local::{asynchronous, blocking::LocalRequest};
use crate::rocket::{Rocket, Cargo}; 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 struct Client {
pub(crate) inner: asynchronous::Client, pub(crate) inner: asynchronous::Client,
runtime: RefCell<tokio::runtime::Runtime>, runtime: RefCell<tokio::runtime::Runtime>,
} }
}
impl Client { impl Client {
fn _new(rocket: Rocket, tracked: bool) -> Result<Client, LaunchError> { fn _new(rocket: Rocket, tracked: bool) -> Result<Client, LaunchError> {

View File

@ -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 contains the `blocking` variant of the `local` API: it can be
//! //! used in Rust's synchronous `#[test]` harness. This is accomplished by
//! This module allows for simple request dispatching against a local, //! starting and running an interal asynchronous Runtime as needed.
//! 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
mod client; mod client;
mod request; mod request;

View File

@ -3,11 +3,31 @@ use std::borrow::Cow;
use crate::{Request, http::Method, local::asynchronous}; use crate::{Request, http::Method, local::asynchronous};
use super::{Client, LocalResponse}; 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)] #[derive(Clone)]
pub struct LocalRequest<'c> { pub struct LocalRequest<'c> {
inner: asynchronous::LocalRequest<'c>, inner: asynchronous::LocalRequest<'c>,
client: &'c Client, client: &'c Client,
} }
}
impl<'c> LocalRequest<'c> { impl<'c> LocalRequest<'c> {
#[inline] #[inline]

View File

@ -2,10 +2,12 @@ use crate::{Response, local::asynchronous};
use super::Client; use super::Client;
struct_response! {
pub struct LocalResponse<'c> { pub struct LocalResponse<'c> {
pub(in super) inner: asynchronous::LocalResponse<'c>, pub(in super) inner: asynchronous::LocalResponse<'c>,
pub(in super) client: &'c Client, pub(in super) client: &'c Client,
} }
}
impl<'c> LocalResponse<'c> { impl<'c> LocalResponse<'c> {
fn _response(&self) -> &Response<'c> { fn _response(&self) -> &Response<'c> {

View File

@ -1,66 +1,40 @@
//! A structure to construct requests for local dispatching. macro_rules! struct_client {
//! ([$(#[$attr:meta])*] $item:item) =>
//! # Usage {
//! /// A structure to construct requests for local dispatching.
//! A `Client` is constructed via the [`new()`] or [`untracked()`] methods from ///
//! an already constructed `Rocket` instance. Once a value of `Client` has been /// # Usage
//! constructed, the [`LocalRequest`] constructor methods ([`get()`], [`put()`], ///
//! [`post()`], and so on) can be used to create a `LocalRequest` for /// A `Client` is constructed via the [`new()`] or [`untracked()`] methods from
//! dispatching. /// an already constructed `Rocket` instance. Once a value of `Client` has been
//! /// constructed, the [`LocalRequest`] constructor methods ([`get()`], [`put()`],
//! See the [top-level documentation](crate::local) for more usage information. /// [`post()`], and so on) can be used to create a `LocalRequest` for
//! /// dispatching.
//! ## Cookie Tracking ///
//! /// See the [top-level documentation](crate::local) for more usage information.
//! A `Client` constructed using [`new()`] propagates cookie changes made by ///
//! responses to previously dispatched requests. In other words, if a previously /// ## Cookie Tracking
//! dispatched request resulted in a response that adds a cookie, any future ///
//! requests will contain that cookie. Similarly, cookies removed by a response /// A `Client` constructed using [`new()`] propagates cookie changes made by
//! won't be propagated further. /// responses to previously dispatched requests. In other words, if a previously
//! /// dispatched request resulted in a response that adds a cookie, any future
//! This is typically the desired mode of operation for a `Client` as it removes /// requests will contain that cookie. Similarly, cookies removed by a response
//! the burden of manually tracking cookies. Under some circumstances, however, /// won't be propagated further.
//! disabling this tracking may be desired. In these cases, use the ///
//! [`untracked()`](Client::untracked()) constructor to create a `Client` that /// This is typically the desired mode of operation for a `Client` as it removes
//! _will not_ track cookies. /// the burden of manually tracking cookies. Under some circumstances, however,
//! /// disabling this tracking may be desired. In these cases, use the
//! ### Synchronization /// [`untracked()`](Client::untracked()) constructor to create a `Client` that
//! /// _will not_ track cookies.
//! While `Client` implements `Sync`, using it in a multithreaded environment $(#[$attr])*
//! while tracking cookies can result in surprising, non-deterministic behavior. ///
//! This is because while cookie modifications are serialized, the exact /// [`new()`]: #method.new
//! ordering depends on when requests are dispatched. Specifically, when cookie /// [`untracked()`]: #method.untracked
//! tracking is enabled, all request dispatches are serialized, which in-turn /// [`get()`]: #method.get
//! serializes modifications to the internally tracked cookies. /// [`put()`]: #method.put
//! /// [`post()`]: #method.post
//! If possible, refrain from sharing a single instance of `Client` across $item
//! 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! req_method { macro_rules! req_method {
($import:literal, $NAME:literal, $f:ident, $method:expr) => ( ($import:literal, $NAME:literal, $f:ident, $method:expr) => (

View File

@ -7,7 +7,14 @@
//! //!
//! # Usage //! # 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 //! [`LocalRequest`] structures that can be dispatched against a given
//! [`Rocket`](crate::Rocket) instance. Usage is straightforward: //! [`Rocket`](crate::Rocket) instance. Usage is straightforward:
//! //!
@ -104,7 +111,7 @@
//! ``` //! ```
//! //!
//! [`Client`]: crate::local::asynchronous::Client //! [`Client`]: crate::local::asynchronous::Client
//! [`LocalRequest`]: crate::local::LocalRequest //! [`LocalRequest`]: crate::local::asynchronous::LocalRequest
#[macro_use] mod client; #[macro_use] mod client;
#[macro_use] mod request; #[macro_use] mod request;

View File

@ -1,62 +1,38 @@
//! A structure representing a local request as created by [`Client`]. macro_rules! struct_request {
//! ([$(#[$attr:meta])*] $item:item) =>
//! # Usage {
//! /// A structure representing a local request as created by [`Client`].
//! A `LocalRequest` value is constructed via method constructors on [`Client`]. ///
//! Headers can be added via the [`header`] builder method and the /// # Usage
//! [`add_header`] method. Cookies can be added via the [`cookie`] builder ///
//! method. The remote IP address can be set via the [`remote`] builder method. /// A `LocalRequest` value is constructed via method constructors on [`Client`].
//! The body of the request can be set via the [`body`] builder method or /// Headers can be added via the [`header`] builder method and the
//! [`set_body`] method. /// [`add_header`] method. Cookies can be added via the [`cookie`] builder
//! /// method. The remote IP address can be set via the [`remote`] builder method.
//! ## Example /// The body of the request can be set via the [`body`] builder method or
//! /// [`set_body`] method.
//! The following snippet uses the available builder methods to construct a ///
//! `POST` request to `/` with a JSON body: $(#[$attr])*
//! ///
//! ```rust /// # Dispatching
//! use rocket::local::asynchronous::Client; ///
//! use rocket::http::{ContentType, Cookie}; /// A `LocalRequest` is dispatched by calling [`dispatch`].
//! /// The `LocalRequest` is consumed and a response is returned.
//! # rocket::async_test(async { ///
//! let client = Client::new(rocket::ignite()).await.expect("valid rocket"); /// Note that `LocalRequest` implements `Clone`. As such, if the
//! let req = client.post("/") /// same request needs to be dispatched multiple times, the request can first be
//! .header(ContentType::JSON) /// cloned and then dispatched: `request.clone().dispatch()`.
//! .remote("127.0.0.1:8000".parse().unwrap()) ///
//! .cookie(Cookie::new("name", "value")) /// [`Client`]: super::Client
//! .body(r#"{ "value": 42 }"#); /// [`header`]: #method.header
//! # }); /// [`add_header`]: #method.add_header
//! ``` /// [`cookie`]: #method.cookie
//! /// [`remote`]: #method.remote
//! # Dispatching /// [`body`]: #method.body
//! /// [`set_body`]: #method.set_body
//! A `LocalRequest` can be dispatched in one of two ways: /// [`dispatch`]: #method.dispatch
//! $item
//! 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! impl_request { macro_rules! impl_request {
($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) =>

View File

@ -1,10 +1,17 @@
//! A structure representing a response from dispatching a local request. macro_rules! struct_response {
//! ($item:item) =>
//! This structure is a thin wrapper around [`Response`]. It implements no {
//! methods of its own; all functionality is exposed via the [`Deref`] and /// A structure representing a response from dispatching a local request.
//! [`DerefMut`] implementations with a target of `Response`. In other words, ///
//! when invoking methods, a `LocalResponse` can be treated exactly as if it /// This structure is a thin wrapper around [`Response`]. It implements no
//! were a `Response`. /// 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 { macro_rules! impl_response {
($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) => ($import:literal $(@$prefix:tt $suffix:tt)? $name:ident) =>