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::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<RwLock<CookieJar>>,
}
}
impl Client {
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 request;
mod response;

View File

@ -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<u8>,
uri: Cow<'c, str>,
}
}
impl<'c> LocalRequest<'c> {
pub(crate) fn new(

View File

@ -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> {

View File

@ -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<tokio::runtime::Runtime>,
}
}
impl Client {
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 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;

View File

@ -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]

View File

@ -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> {

View File

@ -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) => (

View File

@ -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;

View File

@ -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) =>

View File

@ -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) =>