From 6f1c2a75ce52eeeb27272256e6861f82d0ddd0a0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 20 May 2024 00:07:44 -0500 Subject: [PATCH] wip: testbench, locations, display hack, either display hack: when guard errors occur, log the error message with `Display` if an impl exists, falling back to `Debug` otherwise item locations: log the file and line for catchers and routes either: impls `FromParam` for `Either`. changes the implementation of `FromParam` for all `T: FromStr` to return the actual error when the conversion fails instead of the previous `&str` for the value that failed to parse. This results in better log error messages on guard failure. To regain the ability to recover the value that failed to parse, `FromParam` was implemented for `Either`; `Either` can be used as the previous `Result` could, with the benefit that it now works for all `T`. `FromUriParam` and `UriDisplay` were also implemented for `Either`. testbench: fix the testbench for new logging and test that display hack works as intended for both `T: Display` and `T: !Display + Debug`. --- contrib/db_pools/lib/src/pool.rs | 2 +- core/codegen/src/attribute/catch/mod.rs | 3 +- core/codegen/src/attribute/route/mod.rs | 37 +- core/codegen/src/exports.rs | 1 + core/codegen/src/lib.rs | 2 +- .../ui-fail-stable/catch_type_errors.stderr | 8 +- .../ui-fail-stable/responder-types.stderr | 6 +- core/http/src/uri/fmt/from_uri_param.rs | 23 +- core/http/src/uri/fmt/uri_display.rs | 12 + core/lib/src/catcher/catcher.rs | 9 +- core/lib/src/error.rs | 56 ++ core/lib/src/lib.rs | 3 +- core/lib/src/lifecycle.rs | 8 +- core/lib/src/request/from_param.rs | 76 ++- core/lib/src/response/responder.rs | 6 +- core/lib/src/route/route.rs | 6 + core/lib/src/trace/subscriber/pretty.rs | 16 +- core/lib/src/trace/traceable.rs | 8 +- core/lib/tests/sentinel.rs | 2 +- examples/hello/src/main.rs | 3 - examples/responders/src/main.rs | 2 +- testbench/Cargo.toml | 1 + testbench/src/client.rs | 17 +- testbench/src/config.rs | 62 +++ testbench/src/main.rs | 488 +----------------- testbench/src/runner.rs | 74 +++ testbench/src/servers/bind.rs | 45 ++ testbench/src/servers/ignite_failure.rs | 19 + testbench/src/servers/infinite_stream.rs | 29 ++ testbench/src/servers/mod.rs | 8 + testbench/src/servers/mtls.rs | 50 ++ testbench/src/servers/sni_resolver.rs | 141 +++++ testbench/src/servers/tls.rs | 58 +++ testbench/src/servers/tls_resolver.rs | 80 +++ testbench/src/servers/tracing.rs | 123 +++++ 35 files changed, 949 insertions(+), 535 deletions(-) create mode 100644 testbench/src/config.rs create mode 100644 testbench/src/runner.rs create mode 100644 testbench/src/servers/bind.rs create mode 100644 testbench/src/servers/ignite_failure.rs create mode 100644 testbench/src/servers/infinite_stream.rs create mode 100644 testbench/src/servers/mod.rs create mode 100644 testbench/src/servers/mtls.rs create mode 100644 testbench/src/servers/sni_resolver.rs create mode 100644 testbench/src/servers/tls.rs create mode 100644 testbench/src/servers/tls_resolver.rs create mode 100644 testbench/src/servers/tracing.rs diff --git a/contrib/db_pools/lib/src/pool.rs b/contrib/db_pools/lib/src/pool.rs index 9661a17aff..2158016330 100644 --- a/contrib/db_pools/lib/src/pool.rs +++ b/contrib/db_pools/lib/src/pool.rs @@ -156,7 +156,7 @@ pub trait Pool: Sized + Send + Sync + 'static { mod deadpool_postgres { use deadpool::{managed::{Manager, Pool, PoolError, Object, BuildError}, Runtime}; use super::{Duration, Error, Config, Figment}; - use rocket::Either; + use rocket::either::Either; pub trait DeadManager: Manager + Sized + Send + Sync + 'static { fn new(config: &Config) -> Result; diff --git a/core/codegen/src/attribute/catch/mod.rs b/core/codegen/src/attribute/catch/mod.rs index 09528c71e2..57c898a059 100644 --- a/core/codegen/src/attribute/catch/mod.rs +++ b/core/codegen/src/attribute/catch/mod.rs @@ -80,9 +80,10 @@ pub fn _catch( } #_catcher::StaticInfo { - name: stringify!(#user_catcher_fn_name), + name: ::core::stringify!(#user_catcher_fn_name), code: #status_code, handler: monomorphized_function, + location: (::core::file!(), ::core::line!(), ::core::column!()), } } diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index e19b409c89..734574bf55 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -120,18 +120,25 @@ fn query_decls(route: &Route) -> Option { fn request_guard_decl(guard: &Guard) -> TokenStream { let (ident, ty) = (guard.fn_ident.rocketized(), &guard.ty); define_spanned_export!(ty.span() => - __req, __data, _request, FromRequest, Outcome + __req, __data, _request, display_hack, FromRequest, Outcome ); quote_spanned! { ty.span() => let #ident: #ty = match <#ty as #FromRequest>::from_request(#__req).await { #Outcome::Success(__v) => __v, #Outcome::Forward(__e) => { - ::rocket::info!(type_name = stringify!(#ty), "guard forwarding"); + ::rocket::info!(name: "forward", parameter = stringify!(#ident), + type_name = stringify!(#ty), status = __e.code, + "request guard forwarding"); + return #Outcome::Forward((#__data, __e)); }, + #[allow(unreachable_code)] #Outcome::Error((__c, __e)) => { - ::rocket::info!(type_name = stringify!(#ty), "guard failed: {__e:?}"); + ::rocket::info!(name: "failure", parameter = stringify!(#ident), + type_name = stringify!(#ty), reason = %#display_hack!(__e), + "request guard failed"); + return #Outcome::Error(__c); } }; @@ -142,14 +149,14 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { let (i, name, ty) = (guard.index, &guard.name, &guard.ty); define_spanned_export!(ty.span() => __req, __data, _None, _Some, _Ok, _Err, - Outcome, FromSegments, FromParam, Status + Outcome, FromSegments, FromParam, Status, display_hack ); // Returned when a dynamic parameter fails to parse. let parse_error = quote!({ - ::rocket::info!(name: "forward", - reason = %__error, parameter = #name, "type" = stringify!(#ty), - "parameter forwarding"); + ::rocket::info!(name: "forward", parameter = #name, + type_name = stringify!(#ty), reason = %#display_hack!(__error), + "path guard forwarding"); #Outcome::Forward((#__data, #Status::UnprocessableEntity)) }); @@ -161,6 +168,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { match #__req.routed_segment(#i) { #_Some(__s) => match <#ty as #FromParam>::from_param(__s) { #_Ok(__v) => __v, + #[allow(unreachable_code)] #_Err(__error) => return #parse_error, }, #_None => { @@ -176,6 +184,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { true => quote_spanned! { ty.span() => match <#ty as #FromSegments>::from_segments(#__req.routed_segments(#i..)) { #_Ok(__v) => __v, + #[allow(unreachable_code)] #_Err(__error) => return #parse_error, } }, @@ -187,17 +196,24 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { fn data_guard_decl(guard: &Guard) -> TokenStream { let (ident, ty) = (guard.fn_ident.rocketized(), &guard.ty); - define_spanned_export!(ty.span() => __req, __data, FromData, Outcome); + define_spanned_export!(ty.span() => __req, __data, display_hack, FromData, Outcome); quote_spanned! { ty.span() => let #ident: #ty = match <#ty as #FromData>::from_data(#__req, #__data).await { #Outcome::Success(__d) => __d, #Outcome::Forward((__d, __e)) => { - ::rocket::info!(type_name = stringify!(#ty), "data guard forwarding"); + ::rocket::info!(name: "forward", parameter = stringify!(#ident), + type_name = stringify!(#ty), status = __e.code, + "data guard forwarding"); + return #Outcome::Forward((__d, __e)); } + #[allow(unreachable_code)] #Outcome::Error((__c, __e)) => { - ::rocket::info!(type_name = stringify!(#ty), "data guard failed: {__e:?}"); + ::rocket::info!(name: "failure", parameter = stringify!(#ident), + type_name = stringify!(#ty), reason = %#display_hack!(__e), + "data guard failed"); + return #Outcome::Error(__c); } }; @@ -383,6 +399,7 @@ fn codegen_route(route: Route) -> Result { format: #format, rank: #rank, sentinels: #sentinels, + location: (::core::file!(), ::core::line!(), ::core::column!()), } } diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs index 323576aeb6..50470b46b9 100644 --- a/core/codegen/src/exports.rs +++ b/core/codegen/src/exports.rs @@ -86,6 +86,7 @@ define_exported_paths! { _Vec => ::std::vec::Vec, _Cow => ::std::borrow::Cow, _ExitCode => ::std::process::ExitCode, + display_hack => ::rocket::error::display_hack, BorrowMut => ::std::borrow::BorrowMut, Outcome => ::rocket::outcome::Outcome, FromForm => ::rocket::form::FromForm, diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 8dda938862..39401f1c5d 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1328,7 +1328,7 @@ pub fn catchers(input: TokenStream) -> TokenStream { /// assert_eq!(bob2.to_string(), "/person/Bob%20Smith"); /// /// #[get("/person/")] -/// fn ok(age: Result) { } +/// fn ok(age: Result) { } /// /// let kid1 = uri!(ok(age = 10)); /// let kid2 = uri!(ok(12)); diff --git a/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr b/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr index 9d718a2fae..844df3c831 100644 --- a/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr @@ -10,7 +10,7 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> as Responder<'r, 'o>> > as Responder<'r, 'r>> @@ -29,7 +29,7 @@ error[E0277]: the trait bound `bool: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> as Responder<'r, 'o>> > as Responder<'r, 'r>> @@ -62,7 +62,7 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> as Responder<'r, 'o>> > as Responder<'r, 'r>> @@ -81,7 +81,7 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> as Responder<'r, 'o>> > as Responder<'r, 'r>> diff --git a/core/codegen/tests/ui-fail-stable/responder-types.stderr b/core/codegen/tests/ui-fail-stable/responder-types.stderr index bce840f4e1..75b5198513 100644 --- a/core/codegen/tests/ui-fail-stable/responder-types.stderr +++ b/core/codegen/tests/ui-fail-stable/responder-types.stderr @@ -12,7 +12,7 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -52,7 +52,7 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -117,7 +117,7 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> as Responder<'r, 'o>> - as Responder<'r, 'o>> + as Responder<'r, 'o>> and $N others note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs diff --git a/core/http/src/uri/fmt/from_uri_param.rs b/core/http/src/uri/fmt/from_uri_param.rs index f1c5fc01ea..bc25171467 100644 --- a/core/http/src/uri/fmt/from_uri_param.rs +++ b/core/http/src/uri/fmt/from_uri_param.rs @@ -1,5 +1,7 @@ -use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; +use std::collections::{BTreeMap, HashMap}; + +use either::Either; use crate::uri::fmt::UriDisplay; use crate::uri::fmt::{self, Part}; @@ -61,7 +63,7 @@ use crate::uri::fmt::{self, Part}; /// /// * `String`, `i8`, `i16`, `i32`, `i64`, `i128`, `isize`, `u8`, `u16`, /// `u32`, `u64`, `u128`, `usize`, `f32`, `f64`, `bool`, `IpAddr`, -/// `Ipv4Addr`, `Ipv6Addr`, `&str`, `Cow` +/// `Ipv4Addr`, `Ipv6Addr`, `&str`, `Cow`, `Either` /// /// The following types have _identity_ implementations _only in [`Path`]_: /// @@ -375,7 +377,9 @@ impl> FromUriParam for Option } /// A no cost conversion allowing `T` to be used in place of an `Result`. -impl> FromUriParam for Result { +impl FromUriParam for Result + where T: FromUriParam +{ type Target = T::Target; #[inline(always)] @@ -384,6 +388,19 @@ impl> FromUriParam for Result< } } +impl FromUriParam> for Either + where T: FromUriParam, U: FromUriParam +{ + type Target = Either; + + fn from_uri_param(param: Either) -> Self::Target { + match param { + Either::Left(a) => Either::Left(T::from_uri_param(a)), + Either::Right(b) => Either::Right(U::from_uri_param(b)), + } + } +} + impl> FromUriParam> for Option { type Target = Option; diff --git a/core/http/src/uri/fmt/uri_display.rs b/core/http/src/uri/fmt/uri_display.rs index 271b2f55ae..621f3337ba 100644 --- a/core/http/src/uri/fmt/uri_display.rs +++ b/core/http/src/uri/fmt/uri_display.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use std::{fmt, path}; use std::borrow::Cow; +use either::Either; use time::{macros::format_description, format_description::FormatItem}; use crate::RawStr; @@ -421,6 +422,17 @@ impl + ?Sized> UriDisplay

for &T { } } +/// Defers to `T` or `U` in `Either`. +impl, U: UriDisplay

> UriDisplay

for Either { + #[inline(always)] + fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { + match self { + Either::Left(t) => UriDisplay::fmt(t, f), + Either::Right(u) => UriDisplay::fmt(u, f), + } + } +} + /// Defers to the `UriDisplay

` implementation for `T`. impl + ?Sized> UriDisplay

for &mut T { #[inline(always)] diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 2a82b05c47..2aa1402ada 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -127,6 +127,9 @@ pub struct Catcher { /// /// This is -(number of nonempty segments in base). pub(crate) rank: isize, + + /// The catcher's file, line, and column location. + pub(crate) location: Option<(&'static str, u32, u32)>, } // The rank is computed as -(number of nonempty segments in base) => catchers @@ -185,7 +188,8 @@ impl Catcher { base: uri::Origin::root().clone(), handler: Box::new(handler), rank: rank(uri::Origin::root().path()), - code + code, + location: None, } } @@ -328,6 +332,8 @@ pub struct StaticInfo { pub code: Option, /// The catcher's handler, i.e, the annotated function. pub handler: for<'r> fn(Status, &'r Request<'_>) -> BoxFuture<'r>, + /// The file, line, and column where the catcher was defined. + pub location: (&'static str, u32, u32), } #[doc(hidden)] @@ -336,6 +342,7 @@ impl From for Catcher { fn from(info: StaticInfo) -> Catcher { let mut catcher = Catcher::new(info.code, info.handler); catcher.name = Some(info.name.into()); + catcher.location = Some(info.location); catcher } } diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 5f56af39af..5d84110f0c 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -215,3 +215,59 @@ pub(crate) fn log_server_error(error: &(dyn StdError + 'static)) { }); } } + +#[doc(hidden)] +#[macro_export] +macro_rules! display_hack { + ($v:expr) => ({ + #[allow(unused_imports)] + use $crate::error::display_hack_impl::{DisplayHack, DefaultDisplay as _}; + + #[allow(unreachable_code)] + DisplayHack($v).display() + }) +} + +#[doc(hidden)] +pub use display_hack as display_hack; + +#[doc(hidden)] +pub mod display_hack_impl { + use super::*; + use crate::util::Formatter; + + /// The *magic*. + /// + /// This type implements a `display()` method for all types that are either + /// `fmt::Display` _or_ `fmt::Debug`, using the former when available. It + /// does so by using a "specialization" hack: it has a blanket + /// DefaultDisplay trait impl for all types that are `fmt::Debug` and a + /// "specialized" inherent impl for all types that are `fmt::Display`. + /// + /// As long as `T: Display`, the "specialized" impl is what Rust will + /// resolve `DisplayHack(v).display()` to when `T: fmt::Display` as it is an + /// inherent impl. Otherwise, Rust will fall back to the blanket impl. + pub struct DisplayHack(pub T); + + pub trait DefaultDisplay { + fn display(&self) -> impl fmt::Display; + } + + /// Blanket implementation for `T: Debug`. This is what Rust will resolve + /// `DisplayHack::display` to when `T: Debug`. + impl DefaultDisplay for DisplayHack { + #[inline(always)] + fn display(&self) -> impl fmt::Display { + Formatter(|f| fmt::Debug::fmt(&self.0, f)) + } + } + + /// "Specialized" implementation for `T: Display`. This is what Rust will + /// resolve `DisplayHack::display` to when `T: Display`. + impl DisplayHack { + #[inline(always)] + pub fn display(&self) -> impl fmt::Display + '_ { + Formatter(|f| fmt::Display::fmt(&self.0, f)) + } + } +} diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index b5cbc0339d..7ba5ca460d 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -123,6 +123,7 @@ pub use tokio; pub use figment; pub use time; pub use tracing; +pub use either; #[macro_use] pub mod trace; @@ -164,8 +165,6 @@ mod router; mod phase; mod erased; -#[doc(hidden)] pub use either::Either; - #[doc(inline)] pub use rocket_codegen::*; #[doc(inline)] pub use crate::response::Response; diff --git a/core/lib/src/lifecycle.rs b/core/lib/src/lifecycle.rs index 6dccd0580a..6f51c959e7 100644 --- a/core/lib/src/lifecycle.rs +++ b/core/lib/src/lifecycle.rs @@ -1,12 +1,12 @@ use futures::future::{FutureExt, Future}; -use crate::{route, catcher, Rocket, Orbit, Request, Response, Data}; use crate::trace::Trace; use crate::util::Formatter; use crate::data::IoHandler; use crate::http::{Method, Status, Header}; use crate::outcome::Outcome; use crate::form::Form; +use crate::{route, catcher, Rocket, Orbit, Request, Response, Data}; // A token returned to force the execution of one method before another. pub(crate) struct RequestToken; @@ -199,6 +199,7 @@ impl Rocket { let mut status = Status::NotFound; for route in self.router.route(request) { // Retrieve and set the requests parameters. + route.trace_info(); request.set_route(route); let name = route.name.as_deref(); @@ -207,7 +208,6 @@ impl Rocket { // Check if the request processing completed (Some) or if the // request needs to be forwarded. If it does, continue the loop - route.trace_info(); outcome.trace_info(); match outcome { o@Outcome::Success(_) | o@Outcome::Error(_) => return o, @@ -215,9 +215,7 @@ impl Rocket { } } - let outcome = Outcome::Forward((data, status)); - outcome.trace_info(); - outcome + Outcome::Forward((data, status)) } // Invokes the catcher for `status`. Returns the response on success. diff --git a/core/lib/src/request/from_param.rs b/core/lib/src/request/from_param.rs index d53a2cb399..9df9842254 100644 --- a/core/lib/src/request/from_param.rs +++ b/core/lib/src/request/from_param.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use std::path::PathBuf; use crate::error::Empty; +use crate::either::Either; use crate::http::uri::{Segments, error::PathError, fmt::Path}; /// Trait to convert a dynamic path segment string to a concrete value. @@ -40,29 +41,56 @@ use crate::http::uri::{Segments, error::PathError, fmt::Path}; /// /// Sometimes, a forward is not desired, and instead, we simply want to know /// that the dynamic path segment could not be parsed into some desired type -/// `T`. In these cases, types of `Option` or `Result` can be -/// used. These types implement `FromParam` themselves. Their implementations -/// always return successfully, so they never forward. They can be used to -/// determine if the `FromParam` call failed and to retrieve the error value -/// from the failed `from_param` call. +/// `T`. In these cases, types of `Option`, `Result`, or +/// `Either` can be used, which implement `FromParam` themselves. /// -/// For instance, imagine you've asked for an `` as a `usize`. To determine -/// when the `` was not a valid `usize` and retrieve the string that failed -/// to parse, you can use a `Result` type for the `` parameter -/// as follows: +/// * **`Option`** _where_ **`T: FromParam`** +/// +/// Always returns successfully. +/// +/// If the conversion to `T` fails, `None` is returned. If the conversion +/// succeeds, `Some(value)` is returned. +/// +/// * **`Result`** _where_ **`T: FromParam`** +/// +/// Always returns successfully. +/// +/// If the conversion to `T` fails, `Err(error)` is returned. If the +/// conversion succeeds, `Ok(value)` is returned. +/// +/// * **`Either`** _where_ **`A: FromParam`** _and_ **`B: FromParam`** +/// +/// Fails only when both `A::from_param` and `B::from_param` fail. If one +/// of the two succeeds, the successful value is returned in +/// `Either::Left(A)` or `Either::Right(B)` variant, respectively. If both +/// fail, the error values from both calls are returned in a tuple in the +/// `Err` variant. +/// +/// `Either` is particularly useful with a `B` type of `&str`, allowing +/// you to retrieve the invalid path segment. Because `&str`'s implementation of +/// `FromParam` always succeeds, the `Right` variant of the `Either` will always +/// contain the path segment in case of failure. +/// +/// For instance, consider the following route and handler: /// /// ```rust /// # #[macro_use] extern crate rocket; +/// use rocket::either::{Either, Left, Right}; +/// /// #[get("/")] -/// fn hello(id: Result) -> String { +/// fn hello(id: Either) -> String { /// match id { -/// Ok(id_num) => format!("usize: {}", id_num), -/// Err(string) => format!("Not a usize: {}", string) +/// Left(id_num) => format!("usize: {}", id_num), +/// Right(string) => format!("Not a usize: {}", string) /// } /// } /// # fn main() { } /// ``` /// +/// In the above example, if the dynamic path segment cannot be parsed into a +/// `usize`, the raw path segment is returned in the `Right` variant of the +/// `Either` value. +/// /// # Provided Implementations /// /// Rocket implements `FromParam` for several standard library types. Their @@ -219,11 +247,11 @@ impl<'a> FromParam<'a> for String { macro_rules! impl_with_fromstr { ($($T:ty),+) => ($( impl<'a> FromParam<'a> for $T { - type Error = &'a str; + type Error = <$T as FromStr>::Err; #[inline(always)] fn from_param(param: &'a str) -> Result { - <$T as FromStr>::from_str(param).map_err(|_| param) + <$T as FromStr>::from_str(param) } } )+) @@ -361,3 +389,23 @@ impl<'r, T: FromSegments<'r>> FromSegments<'r> for Option { } } } + +/// Implements `FromParam` for `Either`, where `A` and `B` both implement +/// `FromParam`. If `A::from_param` returns `Ok(a)`, `Either::Left(a)` is +/// returned. If `B::from_param` returns `Ok(b)`, `Either::Right(b)` is +/// returned. If both `A::from_param` and `B::from_param` return `Err(a)` and +/// `Err(b)`, respectively, then `Err((a, b))` is returned. +impl<'v, A: FromParam<'v>, B: FromParam<'v>> FromParam<'v> for Either { + type Error = (A::Error, B::Error); + + #[inline(always)] + fn from_param(param: &'v str) -> Result { + match A::from_param(param) { + Ok(a) => Ok(Either::Left(a)), + Err(a) => match B::from_param(param) { + Ok(b) => Ok(Either::Right(b)), + Err(b) => Err((a, b)), + } + } + } +} diff --git a/core/lib/src/response/responder.rs b/core/lib/src/response/responder.rs index 2bc14a7c51..f8d4c71ac1 100644 --- a/core/lib/src/response/responder.rs +++ b/core/lib/src/response/responder.rs @@ -506,13 +506,13 @@ impl<'r, 'o: 'r, 't: 'o, 'e: 'o, T, E> Responder<'r, 'o> for Result /// Responds with the wrapped `Responder` in `self`, whether it is `Left` or /// `Right`. -impl<'r, 'o: 'r, 't: 'o, 'e: 'o, T, E> Responder<'r, 'o> for crate::Either +impl<'r, 'o: 'r, 't: 'o, 'e: 'o, T, E> Responder<'r, 'o> for either::Either where T: Responder<'r, 't>, E: Responder<'r, 'e> { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { match self { - crate::Either::Left(r) => r.respond_to(req), - crate::Either::Right(r) => r.respond_to(req), + either::Either::Left(r) => r.respond_to(req), + either::Either::Right(r) => r.respond_to(req), } } } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index e4e0c8597f..2305ea2c47 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -176,6 +176,8 @@ pub struct Route { pub format: Option, /// The discovered sentinels. pub(crate) sentinels: Vec, + /// The file, line, and column where the route was defined, if known. + pub(crate) location: Option<(&'static str, u32, u32)>, } impl Route { @@ -250,6 +252,7 @@ impl Route { format: None, sentinels: Vec::new(), handler: Box::new(handler), + location: None, rank, uri, method, } } @@ -371,6 +374,8 @@ pub struct StaticInfo { /// Route-derived sentinels, if any. /// This isn't `&'static [SentryInfo]` because `type_name()` isn't `const`. pub sentinels: Vec, + /// The file, line, and column where the route was defined. + pub location: (&'static str, u32, u32), } #[doc(hidden)] @@ -386,6 +391,7 @@ impl From for Route { rank: info.rank.unwrap_or_else(|| uri.default_rank()), format: info.format, sentinels: info.sentinels.into_iter().collect(), + location: Some(info.location), uri, } } diff --git a/core/lib/src/trace/subscriber/pretty.rs b/core/lib/src/trace/subscriber/pretty.rs index 8407bbcd7b..6dce48be35 100644 --- a/core/lib/src/trace/subscriber/pretty.rs +++ b/core/lib/src/trace/subscriber/pretty.rs @@ -98,7 +98,13 @@ impl LookupSpan<'a>> Layer for RocketFmt { )?; if let Some(name) = data.get("name") { - write!(f, " ({})", name.paint(style.bold().bright()))?; + write!(f, " ({}", name.paint(style.bold().bright()))?; + + if let Some(location) = data.get("location") { + write!(f, " {}", location.paint(style.dim()))?; + } + + write!(f, ")")?; } Ok(()) @@ -113,7 +119,13 @@ impl LookupSpan<'a>> Layer for RocketFmt { write!(f, "{}", &data["uri.base"].paint(style.primary()))?; if let Some(name) = data.get("name") { - write!(f, " ({})", name.paint(style.bold().bright()))?; + write!(f, " ({}", name.paint(style.bold().bright()))?; + + if let Some(location) = data.get("location") { + write!(f, " {}", location.paint(style.dim()))?; + } + + write!(f, ")")?; } Ok(()) diff --git a/core/lib/src/trace/traceable.rs b/core/lib/src/trace/traceable.rs index 073892fa87..572ecb5ba4 100644 --- a/core/lib/src/trace/traceable.rs +++ b/core/lib/src/trace/traceable.rs @@ -147,6 +147,9 @@ impl Trace for Route { uri.base = %self.uri.base(), uri.unmounted = %self.uri.unmounted(), format = self.format.as_ref().map(display), + location = self.location.as_ref() + .map(|(file, line, _)| Formatter(move |f| write!(f, "{file}:{line}"))) + .map(display), } event! { Level::DEBUG, "sentinels", @@ -170,6 +173,9 @@ impl Trace for Catcher { }), rank = self.rank, uri.base = %self.base(), + location = self.location.as_ref() + .map(|(file, line, _)| Formatter(move |f| write!(f, "{file}:{line}"))) + .map(display), } } } @@ -303,7 +309,7 @@ impl Trace for ErrorKind { e.trace(level); } else { event!(level, "error::bind", - ?error, + reason = %error, endpoint = endpoint.as_ref().map(display), "binding to network interface failed" ) diff --git a/core/lib/tests/sentinel.rs b/core/lib/tests/sentinel.rs index b9181fa73a..c171181a7c 100644 --- a/core/lib/tests/sentinel.rs +++ b/core/lib/tests/sentinel.rs @@ -1,4 +1,4 @@ -use rocket::{*, error::ErrorKind::SentinelAborts}; +use rocket::{*, either::Either, error::ErrorKind::SentinelAborts}; #[get("/two")] fn two_states(_one: &State, _two: &State) {} diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 5d66842f82..10dbc09e05 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -38,9 +38,6 @@ fn wave(name: &str, age: u8) -> String { format!("👋 Hello, {} year old named {}!", age, name) } -#[get("//")] -fn f(a: usize, b: usize) { } - // Note: without the `..` in `opt..`, we'd need to pass `opt.emoji`, `opt.name`. // // Try visiting: diff --git a/examples/responders/src/main.rs b/examples/responders/src/main.rs index 4e067095ff..90b65b3be2 100644 --- a/examples/responders/src/main.rs +++ b/examples/responders/src/main.rs @@ -159,7 +159,7 @@ fn not_found(request: &Request<'_>) -> content::RawHtml { /******************************* `Either` Responder ***************************/ -use rocket::Either; +use rocket::either::Either; use rocket::response::content::{RawJson, RawMsgPack}; use rocket::http::uncased::AsUncased; diff --git a/testbench/Cargo.toml b/testbench/Cargo.toml index c640a9720c..01e048fe56 100644 --- a/testbench/Cargo.toml +++ b/testbench/Cargo.toml @@ -13,6 +13,7 @@ procspawn = "1" pretty_assertions = "1.4.0" ipc-channel = "0.18" rustls-pemfile = "2.1" +inventory = "0.3.15" [dependencies.nix] version = "0.28" diff --git a/testbench/src/client.rs b/testbench/src/client.rs index c3f3fda3d2..953b2f907a 100644 --- a/testbench/src/client.rs +++ b/testbench/src/client.rs @@ -1,7 +1,7 @@ -use std::time::Duration; +use std::{str::FromStr, time::Duration}; use reqwest::blocking::{ClientBuilder, RequestBuilder}; -use rocket::http::{ext::IntoOwned, uri::{Absolute, Uri}}; +use rocket::http::{ext::IntoOwned, uri::{Absolute, Uri}, Method}; use crate::{Result, Error, Server}; @@ -26,7 +26,7 @@ impl Client { .connect_timeout(Duration::from_secs(5)) } - pub fn get(&self, server: &Server, url: &str) -> Result { + pub fn request(&self, server: &Server, method: Method, url: &str) -> Result { let uri = match Uri::parse_any(url).map_err(|e| e.into_owned())? { Uri::Origin(uri) => { let proto = if server.tls { "https" } else { "http" }; @@ -45,7 +45,16 @@ impl Client { uri => return Err(Error::InvalidUri(uri.into_owned())), }; - Ok(self.client.get(uri.to_string())) + let method = reqwest::Method::from_str(method.as_str()).unwrap(); + Ok(self.client.request(method, uri.to_string())) + } + + pub fn get(&self, server: &Server, url: &str) -> Result { + self.request(server, Method::Get, url) + } + + pub fn post(&self, server: &Server, url: &str) -> Result { + self.request(server, Method::Post, url) } } diff --git a/testbench/src/config.rs b/testbench/src/config.rs new file mode 100644 index 0000000000..c5d19a97d7 --- /dev/null +++ b/testbench/src/config.rs @@ -0,0 +1,62 @@ +use rocket::{Build, Rocket}; + +use testbench::{Result, Error}; + +pub static DEFAULT_CONFIG: &str = r#" + [default] + address = "tcp:127.0.0.1" + workers = 2 + port = 0 + cli_colors = false + log_level = "debug" + secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" + + [default.shutdown] + grace = 1 + mercy = 1 +"#; + +pub static TLS_CONFIG: &str = r#" + [default.tls] + certs = "{ROCKET}/examples/tls/private/rsa_sha256_cert.pem" + key = "{ROCKET}/examples/tls/private/rsa_sha256_key.pem" +"#; + +pub trait RocketExt { + fn default() -> Self; + fn tls_default() -> Self; + fn reconfigure_with_toml(self, toml: &str) -> Self; +} + +impl RocketExt for Rocket { + fn default() -> Self { + rocket::build().reconfigure_with_toml(DEFAULT_CONFIG) + } + + fn tls_default() -> Self { + rocket::build() + .reconfigure_with_toml(DEFAULT_CONFIG) + .reconfigure_with_toml(TLS_CONFIG) + } + + fn reconfigure_with_toml(self, toml: &str) -> Self { + use rocket::figment::{Figment, providers::{Format, Toml}}; + + let toml = toml.replace("{ROCKET}", rocket::fs::relative!("../")); + let config = Figment::from(self.figment()) + .merge(Toml::string(&toml).nested()); + + self.reconfigure(config) + } +} + +pub fn read(path: &str) -> Result> { + let path = path.replace("{ROCKET}", rocket::fs::relative!("../")); + Ok(std::fs::read(path)?) +} + +pub fn cert(path: &str) -> Result> { + let mut data = std::io::Cursor::new(read(path)?); + let cert = rustls_pemfile::certs(&mut data).last(); + Ok(cert.ok_or(Error::MissingCertificate)??.to_vec()) +} diff --git a/testbench/src/main.rs b/testbench/src/main.rs index ae5c828e4c..3145ea3469 100644 --- a/testbench/src/main.rs +++ b/testbench/src/main.rs @@ -1,481 +1,19 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::process::ExitCode; -use std::time::Duration; +mod runner; +mod servers; +mod config; -use rocket::tokio::net::TcpListener; -use rocket::yansi::Paint; -use rocket::{get, routes, Build, Rocket, State}; -use rocket::listener::{unix::UnixListener, Endpoint}; -use rocket::tls::TlsListener; +pub mod prelude { + pub use rocket::*; + pub use rocket::fairing::*; + pub use rocket::response::stream::*; -use reqwest::{tls::TlsInfo, Identity}; - -use testbench::*; - -static DEFAULT_CONFIG: &str = r#" - [default] - address = "tcp:127.0.0.1" - workers = 2 - port = 0 - cli_colors = false - log_level = "debug" - secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" - - [default.shutdown] - grace = 1 - mercy = 1 -"#; - -static TLS_CONFIG: &str = r#" - [default.tls] - certs = "{ROCKET}/examples/tls/private/rsa_sha256_cert.pem" - key = "{ROCKET}/examples/tls/private/rsa_sha256_key.pem" -"#; - -trait RocketExt { - fn default() -> Self; - fn tls_default() -> Self; - fn reconfigure_with_toml(self, toml: &str) -> Self; -} - -impl RocketExt for Rocket { - fn default() -> Self { - rocket::build().reconfigure_with_toml(DEFAULT_CONFIG) - } - - fn tls_default() -> Self { - rocket::build() - .reconfigure_with_toml(DEFAULT_CONFIG) - .reconfigure_with_toml(TLS_CONFIG) - } - - fn reconfigure_with_toml(self, toml: &str) -> Self { - use rocket::figment::{Figment, providers::{Format, Toml}}; - - let toml = toml.replace("{ROCKET}", rocket::fs::relative!("../")); - let config = Figment::from(self.figment()) - .merge(Toml::string(&toml).nested()); - - self.reconfigure(config) - } -} - -fn read(path: &str) -> Result> { - let path = path.replace("{ROCKET}", rocket::fs::relative!("../")); - Ok(std::fs::read(path)?) -} - -fn cert(path: &str) -> Result> { - let mut data = std::io::Cursor::new(read(path)?); - let cert = rustls_pemfile::certs(&mut data).last(); - Ok(cert.ok_or(Error::MissingCertificate)??.to_vec()) -} - -fn run_fail() -> Result<()> { - use rocket::fairing::AdHoc; - - let server = spawn! { - let fail = AdHoc::try_on_ignite("FailNow", |rocket| async { Err(rocket) }); - Rocket::default().attach(fail) - }; - - if let Err(Error::Liftoff(stdout, _)) = server { - assert!(stdout.contains("ignition failure")); - assert!(stdout.contains("FailNow")); - } else { - panic!("unexpected result: {server:#?}"); - } - - Ok(()) + pub use testbench::{Error, Result, *}; + pub use crate::register; + pub use crate::config::*; } -fn infinite() -> Result<()> { - use rocket::response::stream::TextStream; - - let mut server = spawn! { - #[get("/")] - fn infinite() -> TextStream![&'static str] { - TextStream! { - loop { - yield rocket::futures::future::pending::<&str>().await; - } - } - } +pub use runner::Test; - Rocket::default().mount("/", routes![infinite]) - }?; - - let client = Client::default(); - client.get(&server, "/")?.send()?; - server.terminate()?; - - let stdout = server.read_stdout()?; - assert!(stdout.contains("Rocket has launched on http")); - assert!(stdout.contains("GET /")); - assert!(stdout.contains("Graceful shutdown completed")); - Ok(()) +fn main() -> std::process::ExitCode { + runner::run() } - -fn tls_info() -> Result<()> { - #[get("/")] - fn hello_world(endpoint: &Endpoint) -> String { - format!("Hello, {endpoint}!") - } - - let mut server = spawn! { - Rocket::tls_default().mount("/", routes![hello_world]) - }?; - - let client = Client::default(); - let response = client.get(&server, "/")?.send()?; - let tls = response.extensions().get::().unwrap(); - assert!(!tls.peer_certificate().unwrap().is_empty()); - assert!(response.text()?.starts_with("Hello, https://127.0.0.1")); - - server.terminate()?; - let stdout = server.read_stdout()?; - assert!(stdout.contains("Rocket has launched on https")); - assert!(stdout.contains("Graceful shutdown completed")); - assert!(stdout.contains("GET /")); - - let server = Server::spawn((), |(token, _)| { - let rocket = rocket::build() - .reconfigure_with_toml(TLS_CONFIG) - .mount("/", routes![hello_world]); - - token.with_launch(rocket, |rocket| { - let config = rocket.figment().extract_inner("tls"); - rocket.try_launch_on(async move { - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); - let listener = TcpListener::bind(addr).await?; - TlsListener::from(listener, config?).await - }) - }) - }).unwrap(); - - let client = Client::default(); - let response = client.get(&server, "/")?.send()?; - let tls = response.extensions().get::().unwrap(); - assert!(!tls.peer_certificate().unwrap().is_empty()); - assert!(response.text()?.starts_with("Hello, https://127.0.0.1")); - - Ok(()) -} - -fn tls_resolver() -> Result<()> { - use std::sync::Arc; - use std::sync::atomic::{AtomicUsize, Ordering}; - use rocket::tls::{Resolver, TlsConfig, ClientHello, ServerConfig}; - - struct CountingResolver { - config: Arc, - counter: Arc, - } - - #[rocket::async_trait] - impl Resolver for CountingResolver { - async fn init(rocket: &Rocket) -> rocket::tls::Result { - let config: TlsConfig = rocket.figment().extract_inner("tls")?; - let config = Arc::new(config.server_config().await?); - let counter = rocket.state::>().unwrap().clone(); - Ok(Self { config, counter }) - } - - async fn resolve(&self, _: ClientHello<'_>) -> Option> { - self.counter.fetch_add(1, Ordering::Release); - Some(self.config.clone()) - } - } - - let server = spawn! { - #[get("/count")] - fn count(counter: &State>) -> String { - counter.load(Ordering::Acquire).to_string() - } - - let counter = Arc::new(AtomicUsize::new(0)); - Rocket::tls_default() - .manage(counter) - .mount("/", routes![count]) - .attach(CountingResolver::fairing()) - }?; - - let client = Client::default(); - let response = client.get(&server, "/count")?.send()?; - assert_eq!(response.text()?, "1"); - - // Use a new client so we get a new TLS session. - let client = Client::default(); - let response = client.get(&server, "/count")?.send()?; - assert_eq!(response.text()?, "2"); - Ok(()) -} - -fn test_mtls(mandatory: bool) -> Result<()> { - let server = spawn!(mandatory: bool => { - let mtls_config = format!(r#" - [default.tls.mutual] - ca_certs = "{{ROCKET}}/examples/tls/private/ca_cert.pem" - mandatory = {mandatory} - "#); - - #[get("/")] - fn hello(cert: rocket::mtls::Certificate<'_>) -> String { - format!("{}:{}[{}] {}", cert.serial(), cert.version(), cert.issuer(), cert.subject()) - } - - #[get("/", rank = 2)] - fn hi() -> &'static str { - "Hello!" - } - - Rocket::tls_default() - .reconfigure_with_toml(&mtls_config) - .mount("/", routes![hello, hi]) - })?; - - let pem = read("{ROCKET}/examples/tls/private/client.pem")?; - let client: Client = Client::build() - .identity(Identity::from_pem(&pem)?) - .try_into()?; - - let response = client.get(&server, "/")?.send()?; - assert_eq!(response.text()?, - "611895682361338926795452113263857440769284805738:2\ - [C=US, ST=CA, O=Rocket CA, CN=Rocket Root CA] \ - C=US, ST=California, L=Silicon Valley, O=Rocket, \ - CN=Rocket TLS Example, Email=example@rocket.local"); - - let client = Client::default(); - let response = client.get(&server, "/")?.send(); - if mandatory { - assert!(response.unwrap_err().is_request()); - } else { - assert_eq!(response?.text()?, "Hello!"); - } - - Ok(()) -} - -fn tls_mtls() -> Result<()> { - test_mtls(false)?; - test_mtls(true) -} - -fn sni_resolver() -> Result<()> { - use std::sync::Arc; - use std::collections::HashMap; - - use rocket::http::uri::Host; - use rocket::tls::{Resolver, TlsConfig, ClientHello, ServerConfig}; - - struct SniResolver { - default: Arc, - map: HashMap, Arc> - } - - #[rocket::async_trait] - impl Resolver for SniResolver { - async fn init(rocket: &Rocket) -> rocket::tls::Result { - let default: TlsConfig = rocket.figment().extract_inner("tls")?; - let sni: HashMap, TlsConfig> = rocket.figment().extract_inner("tls.sni")?; - - let default = Arc::new(default.server_config().await?); - let mut map = HashMap::new(); - for (host, config) in sni { - let config = config.server_config().await?; - map.insert(host, Arc::new(config)); - } - - Ok(SniResolver { default, map }) - } - - async fn resolve(&self, hello: ClientHello<'_>) -> Option> { - if let Some(Ok(host)) = hello.server_name().map(Host::parse) { - if let Some(config) = self.map.get(&host) { - return Some(config.clone()); - } - } - - Some(self.default.clone()) - } - } - - static SNI_TLS_CONFIG: &str = r#" - [default.tls] - certs = "{ROCKET}/examples/tls/private/rsa_sha256_cert.pem" - key = "{ROCKET}/examples/tls/private/rsa_sha256_key.pem" - - [default.tls.sni."sni1.dev"] - certs = "{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_cert.pem" - key = "{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_key_pkcs8.pem" - - [default.tls.sni."sni2.dev"] - certs = "{ROCKET}/examples/tls/private/ed25519_cert.pem" - key = "{ROCKET}/examples/tls/private/ed25519_key.pem" - "#; - - let server = spawn! { - #[get("/")] fn index() { } - - Rocket::default() - .reconfigure_with_toml(SNI_TLS_CONFIG) - .mount("/", routes![index]) - .attach(SniResolver::fairing()) - }?; - - let client: Client = Client::build() - .resolve("unknown.dev", server.socket_addr()) - .resolve("sni1.dev", server.socket_addr()) - .resolve("sni2.dev", server.socket_addr()) - .try_into()?; - - let response = client.get(&server, "https://unknown.dev")?.send()?; - let tls = response.extensions().get::().unwrap(); - let expected = cert("{ROCKET}/examples/tls/private/rsa_sha256_cert.pem")?; - assert_eq!(tls.peer_certificate().unwrap(), expected); - - let response = client.get(&server, "https://sni1.dev")?.send()?; - let tls = response.extensions().get::().unwrap(); - let expected = cert("{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_cert.pem")?; - assert_eq!(tls.peer_certificate().unwrap(), expected); - - let response = client.get(&server, "https://sni2.dev")?.send()?; - let tls = response.extensions().get::().unwrap(); - let expected = cert("{ROCKET}/examples/tls/private/ed25519_cert.pem")?; - assert_eq!(tls.peer_certificate().unwrap(), expected); - Ok(()) -} - -fn tcp_unix_listener_fail() -> Result<()> { - let server = spawn! { - Rocket::default().reconfigure_with_toml("[default]\naddress = 123") - }; - - if let Err(Error::Liftoff(stdout, _)) = server { - assert!(stdout.contains("expected: valid TCP (ip) or unix (path)")); - assert!(stdout.contains("default.address")); - } else { - panic!("unexpected result: {server:#?}"); - } - - let server = Server::spawn((), |(token, _)| { - let rocket = Rocket::default().reconfigure_with_toml("[default]\naddress = \"unix:foo\""); - token.launch_with::(rocket) - }); - - if let Err(Error::Liftoff(stdout, _)) = server { - assert!(stdout.contains("invalid tcp endpoint: unix:foo")); - } else { - panic!("unexpected result: {server:#?}"); - } - - let server = Server::spawn((), |(token, _)| { - token.launch_with::(Rocket::default()) - }); - - if let Err(Error::Liftoff(stdout, _)) = server { - assert!(stdout.contains("invalid unix endpoint: tcp:127.0.0.1:8000")); - } else { - panic!("unexpected result: {server:#?}"); - } - - Ok(()) -} - -macro_rules! tests { - ($($f:ident),* $(,)?) => {[ - $(Test { - name: stringify!($f), - run: |_: ()| $f().map_err(|e| e.to_string()), - }),* - ]}; -} - -#[derive(Copy, Clone)] -struct Test { - name: &'static str, - run: fn(()) -> Result<(), String>, -} - -static TESTS: &[Test] = &tests![ - run_fail, infinite, tls_info, tls_resolver, tls_mtls, sni_resolver, - tcp_unix_listener_fail -]; - -fn main() -> ExitCode { - procspawn::init(); - - let filter = std::env::args().nth(1).unwrap_or_default(); - let filtered = TESTS.into_iter().filter(|test| test.name.contains(&filter)); - - println!("running {}/{} tests", filtered.clone().count(), TESTS.len()); - let handles = filtered.map(|test| (test, std::thread::spawn(|| { - let name = test.name; - let start = std::time::SystemTime::now(); - let mut proc = procspawn::spawn((), test.run); - let result = loop { - match proc.join_timeout(Duration::from_secs(10)) { - Err(e) if e.is_timeout() => { - let elapsed = start.elapsed().unwrap().as_secs(); - println!("{name} has been running for {elapsed} seconds..."); - - if elapsed >= 30 { - println!("{name} timeout"); - break Err(e); - } - }, - result => break result, - } - }; - - match result.as_ref().map_err(|e| e.panic_info()) { - Ok(Ok(_)) => println!("test {name} ... {}", "ok".green()), - Ok(Err(e)) => println!("test {name} ... {}\n {e}", "fail".red()), - Err(Some(_)) => println!("test {name} ... {}", "panic".red().underline()), - Err(None) => println!("test {name} ... {}", "error".magenta()), - } - - matches!(result, Ok(Ok(()))) - }))); - - let mut success = true; - for (_, handle) in handles { - success &= handle.join().unwrap_or(false); - } - - match success { - true => ExitCode::SUCCESS, - false => { - println!("note: use `NOCAPTURE=1` to see test output"); - ExitCode::FAILURE - } - } -} - -// TODO: Implement an `UpdatingResolver`. Expose `SniResolver` and -// `UpdatingResolver` in a `contrib` library or as part of `rocket`. -// -// struct UpdatingResolver { -// timestamp: AtomicU64, -// config: ArcSwap -// } -// -// #[crate::async_trait] -// impl Resolver for UpdatingResolver { -// async fn resolve(&self, _: ClientHello<'_>) -> Option> { -// if let Either::Left(path) = self.tls_config.certs() { -// let metadata = tokio::fs::metadata(&path).await.ok()?; -// let modtime = metadata.modified().ok()?; -// let timestamp = modtime.duration_since(UNIX_EPOCH).ok()?.as_secs(); -// let old_timestamp = self.timestamp.load(Ordering::Acquire); -// if timestamp > old_timestamp { -// let new_config = self.tls_config.to_server_config().await.ok()?; -// self.server_config.store(Arc::new(new_config)); -// self.timestamp.store(timestamp, Ordering::Release); -// } -// } -// -// Some(self.server_config.load_full()) -// } -// } diff --git a/testbench/src/runner.rs b/testbench/src/runner.rs new file mode 100644 index 0000000000..bb69e66807 --- /dev/null +++ b/testbench/src/runner.rs @@ -0,0 +1,74 @@ +use std::time::Duration; + +use rocket::yansi::Paint; + +#[derive(Copy, Clone)] +pub struct Test { + pub name: &'static str, + pub run: fn(()) -> Result<(), String>, +} + +#[macro_export] +macro_rules! register { + ($f:ident $( ( $($v:ident: $a:expr),* ) )?) => { + ::inventory::submit!($crate::Test { + name: stringify!($f $(($($v = $a),*))?), + run: |_: ()| $f($($($a),*)?).map_err(|e| e.to_string()), + }); + }; +} + +inventory::collect!(Test); + +pub fn run() -> std::process::ExitCode { + procspawn::init(); + + let filter = std::env::args().nth(1).unwrap_or_default(); + let filtered = inventory::iter:: + .into_iter() + .filter(|t| t.name.contains(&filter)); + + let total_tests = inventory::iter::.into_iter().count(); + println!("running {}/{total_tests} tests", filtered.clone().count()); + let handles = filtered.map(|test| (test, std::thread::spawn(|| { + let name = test.name; + let start = std::time::SystemTime::now(); + let mut proc = procspawn::spawn((), test.run); + let result = loop { + match proc.join_timeout(Duration::from_secs(10)) { + Err(e) if e.is_timeout() => { + let elapsed = start.elapsed().unwrap().as_secs(); + println!("{name} has been running for {elapsed} seconds..."); + + if elapsed >= 30 { + println!("{name} timeout"); + break Err(e); + } + }, + result => break result, + } + }; + + match result.as_ref().map_err(|e| e.panic_info()) { + Ok(Ok(_)) => println!("test {name} ... {}", "ok".green()), + Ok(Err(e)) => println!("test {name} ... {}\n {e}", "fail".red()), + Err(Some(_)) => println!("test {name} ... {}", "panic".red().underline()), + Err(None) => println!("test {name} ... {}", "error".magenta()), + } + + matches!(result, Ok(Ok(()))) + }))); + + let mut success = true; + for (_, handle) in handles { + success &= handle.join().unwrap_or(false); + } + + match success { + true => std::process::ExitCode::SUCCESS, + false => { + println!("note: use `NOCAPTURE=1` to see test output"); + std::process::ExitCode::FAILURE + } + } +} diff --git a/testbench/src/servers/bind.rs b/testbench/src/servers/bind.rs new file mode 100644 index 0000000000..b9981b3cc7 --- /dev/null +++ b/testbench/src/servers/bind.rs @@ -0,0 +1,45 @@ +use rocket::{tokio::net::TcpListener}; + +use crate::prelude::*; + +#[cfg(unix)] +fn tcp_unix_listener_fail() -> Result<()> { + use rocket::listener::unix::UnixListener; + + let server = spawn! { + Rocket::default().reconfigure_with_toml("[default]\naddress = 123") + }; + + if let Err(Error::Liftoff(stdout, _)) = server { + assert!(stdout.contains("expected: valid TCP (ip) or unix (path)")); + assert!(stdout.contains("default.address")); + } else { + panic!("unexpected result: {server:#?}"); + } + + let server = Server::spawn((), |(token, _)| { + let rocket = Rocket::default().reconfigure_with_toml("[default]\naddress = \"unix:foo\""); + token.launch_with::(rocket) + }); + + if let Err(Error::Liftoff(stdout, _)) = server { + assert!(stdout.contains("invalid tcp endpoint: unix:foo")); + } else { + panic!("unexpected result: {server:#?}"); + } + + let server = Server::spawn((), |(token, _)| { + token.launch_with::(Rocket::default()) + }); + + if let Err(Error::Liftoff(stdout, _)) = server { + assert!(stdout.contains("invalid unix endpoint: tcp:127.0.0.1:8000")); + } else { + panic!("unexpected result: {server:#?}"); + } + + Ok(()) +} + +#[cfg(unix)] +register!(tcp_unix_listener_fail); diff --git a/testbench/src/servers/ignite_failure.rs b/testbench/src/servers/ignite_failure.rs new file mode 100644 index 0000000000..6a3da0cdd3 --- /dev/null +++ b/testbench/src/servers/ignite_failure.rs @@ -0,0 +1,19 @@ +use crate::prelude::*; + +fn test_ignite_failure() -> Result<()> { + let server = spawn! { + let fail = AdHoc::try_on_ignite("FailNow", |rocket| async { Err(rocket) }); + Rocket::default().attach(fail) + }; + + if let Err(Error::Liftoff(stdout, _)) = server { + assert!(stdout.contains("ignition failure")); + assert!(stdout.contains("FailNow")); + } else { + panic!("unexpected result: {server:#?}"); + } + + Ok(()) +} + +register!(test_ignite_failure); diff --git a/testbench/src/servers/infinite_stream.rs b/testbench/src/servers/infinite_stream.rs new file mode 100644 index 0000000000..b4a16914c2 --- /dev/null +++ b/testbench/src/servers/infinite_stream.rs @@ -0,0 +1,29 @@ +use crate::prelude::*; + +#[get("/")] +fn infinite() -> TextStream![&'static str] { + TextStream! { + loop { + yield rocket::futures::future::pending::<&str>().await; + } + } +} + +pub fn test_inifinite_streams_end() -> Result<()> { + let mut server = spawn! { + Rocket::default().mount("/", routes![infinite]) + }?; + + let client = Client::default(); + client.get(&server, "/")?.send()?; + server.terminate()?; + + let stdout = server.read_stdout()?; + assert!(stdout.contains("Rocket has launched on http")); + assert!(stdout.contains("GET /")); + assert!(stdout.contains("Graceful shutdown completed")); + + Ok(()) +} + +register!(test_inifinite_streams_end); diff --git a/testbench/src/servers/mod.rs b/testbench/src/servers/mod.rs new file mode 100644 index 0000000000..30fd6ff97c --- /dev/null +++ b/testbench/src/servers/mod.rs @@ -0,0 +1,8 @@ +pub mod ignite_failure; +pub mod bind; +pub mod infinite_stream; +pub mod tls_resolver; +pub mod mtls; +pub mod sni_resolver; +pub mod tracing; +pub mod tls; diff --git a/testbench/src/servers/mtls.rs b/testbench/src/servers/mtls.rs new file mode 100644 index 0000000000..1fdaea7ae0 --- /dev/null +++ b/testbench/src/servers/mtls.rs @@ -0,0 +1,50 @@ +use crate::prelude::*; + +fn test_mtls(mandatory: bool) -> Result<()> { + let server = spawn!(mandatory: bool => { + let mtls_config = format!(r#" + [default.tls.mutual] + ca_certs = "{{ROCKET}}/examples/tls/private/ca_cert.pem" + mandatory = {mandatory} + "#); + + #[get("/")] + fn hello(cert: rocket::mtls::Certificate<'_>) -> String { + format!("{}:{}[{}] {}", cert.serial(), cert.version(), cert.issuer(), cert.subject()) + } + + #[get("/", rank = 2)] + fn hi() -> &'static str { + "Hello!" + } + + Rocket::tls_default() + .reconfigure_with_toml(&mtls_config) + .mount("/", routes![hello, hi]) + })?; + + let pem = read("{ROCKET}/examples/tls/private/client.pem")?; + let client: Client = Client::build() + .identity(reqwest::Identity::from_pem(&pem)?) + .try_into()?; + + let response = client.get(&server, "/")?.send()?; + assert_eq!(response.text()?, + "611895682361338926795452113263857440769284805738:2\ + [C=US, ST=CA, O=Rocket CA, CN=Rocket Root CA] \ + C=US, ST=California, L=Silicon Valley, O=Rocket, \ + CN=Rocket TLS Example, Email=example@rocket.local"); + + let client = Client::default(); + let response = client.get(&server, "/")?.send(); + if mandatory { + assert!(response.unwrap_err().is_request()); + } else { + assert_eq!(response?.text()?, "Hello!"); + } + + Ok(()) +} + +register!(test_mtls(mandatory: true)); +register!(test_mtls(mandatory: false)); diff --git a/testbench/src/servers/sni_resolver.rs b/testbench/src/servers/sni_resolver.rs new file mode 100644 index 0000000000..1e41a05d25 --- /dev/null +++ b/testbench/src/servers/sni_resolver.rs @@ -0,0 +1,141 @@ +use std::sync::Arc; +use std::collections::HashMap; + +use rocket::http::uri::Host; +use rocket::tls::{Resolver, TlsConfig, ClientHello, ServerConfig}; +use reqwest::tls::TlsInfo; + +use crate::prelude::*; + +static SNI_TLS_CONFIG: &str = r#" + [default.tls] + certs = "{ROCKET}/examples/tls/private/rsa_sha256_cert.pem" + key = "{ROCKET}/examples/tls/private/rsa_sha256_key.pem" + + [default.tls.sni."sni1.dev"] + certs = "{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_cert.pem" + key = "{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_key_pkcs8.pem" + + [default.tls.sni."sni2.dev"] + certs = "{ROCKET}/examples/tls/private/ed25519_cert.pem" + key = "{ROCKET}/examples/tls/private/ed25519_key.pem" +"#; + +struct SniResolver { + default: Arc, + map: HashMap, Arc> +} + +#[rocket::async_trait] +impl Resolver for SniResolver { + async fn init(rocket: &Rocket) -> rocket::tls::Result { + let default: TlsConfig = rocket.figment().extract_inner("tls")?; + let sni: HashMap, TlsConfig> = rocket.figment().extract_inner("tls.sni")?; + + let default = Arc::new(default.server_config().await?); + let mut map = HashMap::new(); + for (host, config) in sni { + let config = config.server_config().await?; + map.insert(host, Arc::new(config)); + } + + Ok(SniResolver { default, map }) + } + + async fn resolve(&self, hello: ClientHello<'_>) -> Option> { + if let Some(Ok(host)) = hello.server_name().map(Host::parse) { + if let Some(config) = self.map.get(&host) { + return Some(config.clone()); + } + } + + Some(self.default.clone()) + } +} + +fn sni_resolver() -> Result<()> { + let server = spawn! { + #[get("/")] fn index() { } + + Rocket::default() + .reconfigure_with_toml(SNI_TLS_CONFIG) + .mount("/", routes![index]) + .attach(SniResolver::fairing()) + }?; + + let client: Client = Client::build() + .resolve("unknown.dev", server.socket_addr()) + .resolve("sni1.dev", server.socket_addr()) + .resolve("sni2.dev", server.socket_addr()) + .try_into()?; + + let response = client.get(&server, "https://unknown.dev")?.send()?; + let tls = response.extensions().get::().unwrap(); + let expected = cert("{ROCKET}/examples/tls/private/rsa_sha256_cert.pem")?; + assert_eq!(tls.peer_certificate().unwrap(), expected); + + let response = client.get(&server, "https://sni1.dev")?.send()?; + let tls = response.extensions().get::().unwrap(); + let expected = cert("{ROCKET}/examples/tls/private/ecdsa_nistp256_sha256_cert.pem")?; + assert_eq!(tls.peer_certificate().unwrap(), expected); + + let response = client.get(&server, "https://sni2.dev")?.send()?; + let tls = response.extensions().get::().unwrap(); + let expected = cert("{ROCKET}/examples/tls/private/ed25519_cert.pem")?; + assert_eq!(tls.peer_certificate().unwrap(), expected); + Ok(()) +} + +register!(sni_resolver); + +// struct CountingResolver { +// config: Arc, +// counter: Arc, +// } +// +// #[rocket::async_trait] +// impl Resolver for CountingResolver { +// async fn init(rocket: &Rocket) -> rocket::tls::Result { +// let config: TlsConfig = rocket.figment().extract_inner("tls")?; +// let config = Arc::new(config.server_config().await?); +// let counter = rocket.state::>().unwrap().clone(); +// Ok(Self { config, counter }) +// } +// +// async fn resolve(&self, _: ClientHello<'_>) -> Option> { +// self.counter.fetch_add(1, Ordering::Release); +// Some(self.config.clone()) +// } +// } +// +// #[get("/count")] +// fn count(counter: &State>) -> String { +// counter.load(Ordering::Acquire).to_string() +// } +// +// fn test_tls_resolver() -> Result<()> { +// use std::sync::Arc; +// use std::sync::atomic::{AtomicUsize, Ordering}; +// use rocket::tls::{Resolver, TlsConfig, ClientHello, ServerConfig}; +// +// let server = spawn! { +// let counter = Arc::new(AtomicUsize::new(0)); +// Rocket::tls_default() +// .manage(counter) +// .mount("/", routes![count]) +// .attach(CountingResolver::fairing()) +// }?; +// +// let client = Client::default(); +// let response = client.get(&server, "/count")?.send()?; +// assert_eq!(response.text()?, "1"); +// +// // Use a new client so we get a new TLS session. +// let client = Client::default(); +// let response = client.get(&server, "/count")?.send()?; +// assert_eq!(response.text()?, "2"); +// Ok(()) +// } +// +// register!(test_tls_works); +// register!(test_tls_resolver); diff --git a/testbench/src/servers/tls.rs b/testbench/src/servers/tls.rs new file mode 100644 index 0000000000..7288594588 --- /dev/null +++ b/testbench/src/servers/tls.rs @@ -0,0 +1,58 @@ +use crate::prelude::*; + +use std::net::{Ipv4Addr, SocketAddr}; + +use rocket::tokio::net::TcpListener; +use rocket::{get, routes, Rocket}; +use rocket::listener::Endpoint; +use rocket::tls::TlsListener; + +use reqwest::tls::TlsInfo; + +#[get("/")] +fn hello_world(endpoint: &Endpoint) -> String { + format!("Hello, {endpoint}!") +} + +fn test_tls_works() -> Result<()> { + let mut server = spawn! { + Rocket::tls_default().mount("/", routes![hello_world]) + }?; + + let client = Client::default(); + let response = client.get(&server, "/")?.send()?; + let tls = response.extensions().get::().unwrap(); + assert!(!tls.peer_certificate().unwrap().is_empty()); + assert!(response.text()?.starts_with("Hello, https://127.0.0.1")); + + server.terminate()?; + let stdout = server.read_stdout()?; + assert!(stdout.contains("Rocket has launched on https")); + assert!(stdout.contains("Graceful shutdown completed")); + assert!(stdout.contains("GET /")); + + let server = Server::spawn((), |(token, _)| { + let rocket = rocket::build() + .reconfigure_with_toml(TLS_CONFIG) + .mount("/", routes![hello_world]); + + token.with_launch(rocket, |rocket| { + let config = rocket.figment().extract_inner("tls"); + rocket.try_launch_on(async move { + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); + let listener = TcpListener::bind(addr).await?; + TlsListener::from(listener, config?).await + }) + }) + }).unwrap(); + + let client = Client::default(); + let response = client.get(&server, "/")?.send()?; + let tls = response.extensions().get::().unwrap(); + assert!(!tls.peer_certificate().unwrap().is_empty()); + assert!(response.text()?.starts_with("Hello, https://127.0.0.1")); + + Ok(()) +} + +register!(test_tls_works); diff --git a/testbench/src/servers/tls_resolver.rs b/testbench/src/servers/tls_resolver.rs new file mode 100644 index 0000000000..af23d1157c --- /dev/null +++ b/testbench/src/servers/tls_resolver.rs @@ -0,0 +1,80 @@ +use crate::prelude::*; + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use rocket::tls::{ClientHello, Resolver, ServerConfig, TlsConfig}; + +struct CountingResolver { + config: Arc, + counter: Arc, +} + +#[rocket::async_trait] +impl Resolver for CountingResolver { + async fn init(rocket: &Rocket) -> rocket::tls::Result { + let config: TlsConfig = rocket.figment().extract_inner("tls")?; + let config = Arc::new(config.server_config().await?); + let counter = rocket.state::>().unwrap().clone(); + Ok(Self { config, counter }) + } + + async fn resolve(&self, _: ClientHello<'_>) -> Option> { + self.counter.fetch_add(1, Ordering::Release); + Some(self.config.clone()) + } +} + +#[get("/count")] +fn count(counter: &State>) -> String { + counter.load(Ordering::Acquire).to_string() +} + +fn test_tls_resolver() -> Result<()> { + let server = spawn! { + let counter = Arc::new(AtomicUsize::new(0)); + Rocket::tls_default() + .manage(counter) + .mount("/", routes![count]) + .attach(CountingResolver::fairing()) + }?; + + let client = Client::default(); + let response = client.get(&server, "/count")?.send()?; + assert_eq!(response.text()?, "1"); + + // Use a new client so we get a new TLS session. + let client = Client::default(); + let response = client.get(&server, "/count")?.send()?; + assert_eq!(response.text()?, "2"); + Ok(()) +} + +register!(test_tls_resolver); + +// TODO: Implement an `UpdatingResolver`. Expose `SniResolver` and +// `UpdatingResolver` in a `contrib` library or as part of `rocket`. +// +// struct UpdatingResolver { +// timestamp: AtomicU64, +// config: ArcSwap +// } +// +// #[crate::async_trait] +// impl Resolver for UpdatingResolver { +// async fn resolve(&self, _: ClientHello<'_>) -> Option> { +// if let Either::Left(path) = self.tls_config.certs() { +// let metadata = tokio::fs::metadata(&path).await.ok()?; +// let modtime = metadata.modified().ok()?; +// let timestamp = modtime.duration_since(UNIX_EPOCH).ok()?.as_secs(); +// let old_timestamp = self.timestamp.load(Ordering::Acquire); +// if timestamp > old_timestamp { +// let new_config = self.tls_config.to_server_config().await.ok()?; +// self.server_config.store(Arc::new(new_config)); +// self.timestamp.store(timestamp, Ordering::Release); +// } +// } +// +// Some(self.server_config.load_full()) +// } +// } diff --git a/testbench/src/servers/tracing.rs b/testbench/src/servers/tracing.rs new file mode 100644 index 0000000000..b4ada1b3fa --- /dev/null +++ b/testbench/src/servers/tracing.rs @@ -0,0 +1,123 @@ +//! Check that guard failures result in trace with `Display` message for guard +//! types that implement `Display` and otherwise uses `Debug`. + +use std::fmt; + +use rocket::http::Status; +use rocket::data::{self, FromData}; +use rocket::http::uri::{Segments, fmt::Path}; +use rocket::request::{self, FromParam, FromRequest, FromSegments}; + +use crate::prelude::*; + +#[derive(Debug)] +struct UseDisplay(&'static str); + +#[derive(Debug)] +struct UseDebug; + +impl fmt::Display for UseDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "this is the display impl: {}", self.0) + } +} + +impl FromParam<'_> for UseDisplay { + type Error = Self; + fn from_param(_: &str) -> Result { Err(Self("param")) } +} + +impl FromParam<'_> for UseDebug { + type Error = Self; + fn from_param(_: &str) -> Result { Err(Self) } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for UseDisplay { + type Error = Self; + async fn from_request(_: &'r Request<'_>) -> request::Outcome { + request::Outcome::Error((Status::InternalServerError, Self("req"))) + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for UseDebug { + type Error = Self; + async fn from_request(_: &'r Request<'_>) -> request::Outcome { + request::Outcome::Error((Status::InternalServerError, Self)) + } +} + +#[rocket::async_trait] +impl<'r> FromData<'r> for UseDisplay { + type Error = Self; + async fn from_data(_: &'r Request<'_>, _: Data<'r>) -> data::Outcome<'r, Self> { + data::Outcome::Error((Status::InternalServerError, Self("data"))) + } +} + +#[rocket::async_trait] +impl<'r> FromData<'r> for UseDebug { + type Error = Self; + async fn from_data(_: &'r Request<'_>, _: Data<'r>) -> data::Outcome<'r, Self> { + data::Outcome::Error((Status::InternalServerError, Self)) + } +} + +impl<'r> FromSegments<'r> for UseDisplay { + type Error = Self; + fn from_segments(_: Segments<'r, Path>) -> Result { Err(Self("segment")) } +} + +impl<'r> FromSegments<'r> for UseDebug { + type Error = Self; + fn from_segments(_: Segments<'r, Path>) -> Result { Err(Self) } +} + +pub fn test_display_guard_err() -> Result<()> { + #[get("/<_v>", rank = 1)] fn a(_v: UseDisplay) {} + #[get("/<_v..>", rank = 2)] fn b(_v: UseDisplay) {} + #[get("/<_..>", rank = 3)] fn d(_v: UseDisplay) {} + #[post("/<_..>", data = "<_v>")] fn c(_v: UseDisplay) {} + + let mut server = spawn! { + Rocket::default().mount("/", routes![a, b, c, d]) + }?; + + let client = Client::default(); + client.get(&server, "/foo")?.send()?; + client.post(&server, "/foo")?.send()?; + server.terminate()?; + + let stdout = server.read_stdout()?; + assert!(stdout.contains("this is the display impl: param")); + assert!(stdout.contains("this is the display impl: req")); + assert!(stdout.contains("this is the display impl: segment")); + assert!(stdout.contains("this is the display impl: data")); + + Ok(()) +} + +pub fn test_debug_guard_err() -> Result<()> { + #[get("/<_v>", rank = 1)] fn a(_v: UseDebug) {} + #[get("/<_v..>", rank = 2)] fn b(_v: UseDebug) {} + #[get("/<_..>", rank = 3)] fn d(_v: UseDebug) {} + #[post("/<_..>", data = "<_v>")] fn c(_v: UseDebug) {} + + let mut server = spawn! { + Rocket::default().mount("/", routes![a, b, c, d]) + }?; + + let client = Client::default(); + client.get(&server, "/foo")?.send()?; + client.post(&server, "/foo")?.send()?; + server.terminate()?; + + let stdout = server.read_stdout()?; + assert!(!stdout.contains("this is the display impl")); + assert!(stdout.contains("UseDebug")); + Ok(()) +} + +register!(test_display_guard_err); +register!(test_debug_guard_err);