Skip to main content

trillium_testing/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![deny(
4    clippy::dbg_macro,
5    missing_copy_implementations,
6    rustdoc::missing_crate_level_docs,
7    missing_debug_implementations,
8    missing_docs,
9    nonstandard_style,
10    unused_qualifications
11)]
12
13//! Testing utilities for trillium applications.
14//!
15//! This crate is intended to be used as a development dependency.
16//!
17//! ```
18//! # trillium_testing::block_on(async {
19//! use trillium::{Conn, Status, conn_try};
20//! use trillium_testing::TestServer;
21//! async fn handler(mut conn: Conn) -> Conn {
22//!     let request_body = conn_try!(conn.request_body_string().await, conn);
23//!     conn.with_body(format!("request body was: {}", request_body))
24//!         .with_status(418)
25//!         .with_response_header("request-id", "special-request")
26//! }
27//!
28//! let app = TestServer::new(handler).await;
29//! app.post("/")
30//!     .with_body("hello trillium!")
31//!     .await
32//!     .assert_status(Status::ImATeapot)
33//!     .assert_body("request body was: hello trillium!")
34//!     .assert_headers([("request-id", "special-request"), ("content-length", "33")]);
35//! # });
36//! ```
37//!
38//! ## Features
39//!
40//! To use the same runtime as your application, enable a runtime feature for **trillium testing**.
41//! Without a runtime feature enabled, trillium testing will approximate a runtime through spawning
42//! a thread per task and blocking on a future. This is fine for simple testing, but you probably
43//! want to enable a server feature.
44//!
45//! ### Tokio:
46//! ```toml
47//! [dev-dependencies]
48//! # ...
49//! trillium-testing = { version = "...", features = ["tokio"] }
50//! ```
51//!
52//! ### Async-std:
53//! ```toml
54//! [dev-dependencies]
55//! # ...
56//! trillium-testing = { version = "...", features = ["async-std"] }
57//! ```
58//!
59//! ### Smol:
60//! ```toml
61//! [dev-dependencies]
62//! # ...
63//! trillium-testing = { version = "...", features = ["smol"] }
64//! ```
65
66#[cfg(test)]
67#[doc = include_str!("../README.md")]
68mod readme {}
69
70mod assertions;
71
72mod test_transport;
73use std::{future::Future, process::Termination, sync::Arc};
74pub use test_transport::TestTransport;
75
76mod test_conn;
77pub use test_conn::TestConn;
78
79pub mod methods;
80pub mod prelude {
81    //! useful stuff for testing trillium apps
82    pub use crate::{
83        assert_body, assert_body_contains, assert_headers, assert_not_handled, assert_ok,
84        assert_response, assert_status, block_on, connector, init, methods::*,
85    };
86    pub use trillium::{Conn, Method, Status};
87}
88
89use trillium::{Handler, Info};
90pub use trillium::{Method, Status};
91use trillium_http::HttpContext;
92pub use url::Url;
93
94/// runs the future to completion on the current thread
95pub fn block_on<Fut: Future>(fut: Fut) -> Fut::Output {
96    runtime().block_on(fut)
97}
98
99/// initialize a handler
100pub async fn init(handler: &mut impl Handler) -> Arc<HttpContext> {
101    let mut info = Info::from(HttpContext::default());
102    info.insert_shared_state(runtime());
103    info.insert_shared_state(runtime().into());
104    handler.init(&mut info).await;
105    Arc::new(info.into())
106}
107
108// these exports are used by macros
109pub use futures_lite::{self, AsyncRead, AsyncReadExt, AsyncWrite, Stream};
110
111mod server_connector;
112pub use server_connector::{ServerConnector, connector};
113use trillium_server_common::Config;
114pub use trillium_server_common::{
115    ArcedConnector, Connector, Runtime, RuntimeTrait, Server, ServerHandle,
116};
117
118/// Bound exposed by [`config`]'s `UdpTransport` and [`client_config`]'s `Udp`
119/// when a real runtime feature is enabled. QUIC adapters such as
120/// `trillium-quinn` require raw FD/socket access to drive platform-optimized
121/// UDP I/O.
122#[cfg(unix)]
123#[doc(hidden)]
124pub trait QuicSocket: std::os::unix::io::AsFd {}
125#[cfg(unix)]
126impl<T: std::os::unix::io::AsFd> QuicSocket for T {}
127#[doc(hidden)]
128#[cfg(windows)]
129pub trait QuicSocket: std::os::windows::io::AsSocket {}
130#[cfg(windows)]
131impl<T: std::os::windows::io::AsSocket> QuicSocket for T {}
132
133cfg_if::cfg_if! {
134    if #[cfg(feature = "smol")] {
135        /// runtime server config
136        pub fn config() -> Config<impl Server<Runtime: Unpin, UdpTransport: QuicSocket>, ()> {
137            trillium_smol::config()
138        }
139
140        /// runtime client config
141        pub fn client_config() -> impl Connector<Runtime: Unpin, Udp: QuicSocket> {
142            trillium_smol::ClientConfig::default()
143        }
144        /// smol runtime
145        pub fn runtime() -> impl RuntimeTrait {
146            trillium_smol::SmolRuntime::default()
147        }
148    } else if #[cfg(feature = "async-std")] {
149        /// runtime server config
150        pub fn config() -> Config<impl Server<Runtime: Unpin, UdpTransport: QuicSocket>, ()> {
151            trillium_async_std::config()
152        }
153        /// runtime client config
154        pub fn client_config() -> impl Connector<Runtime: Unpin, Udp: QuicSocket> {
155            trillium_async_std::ClientConfig::default()
156        }
157        /// async std runtime
158        pub fn runtime() -> impl RuntimeTrait {
159            trillium_async_std::AsyncStdRuntime::default()
160        }
161    } else if #[cfg(feature = "tokio")] {
162        /// runtime server config
163        pub fn config() -> Config<impl Server<Runtime: Unpin, UdpTransport: QuicSocket>, ()> {
164            trillium_tokio::config()
165        }
166
167        /// tokio client config
168        pub fn client_config() -> impl Connector<Runtime: Unpin, Udp: QuicSocket> {
169            trillium_tokio::ClientConfig::default()
170        }
171
172        /// tokio runtime
173        pub fn runtime() -> impl RuntimeTrait {
174            trillium_tokio::TokioRuntime::default()
175        }
176   } else {
177        /// runtime server config
178        pub fn config() -> Config<impl Server, ()> {
179            Config::<RuntimelessServer, ()>::new()
180        }
181
182        /// generic client config
183        pub fn client_config() -> impl Connector {
184            RuntimelessClientConfig::default()
185        }
186
187       /// generic runtime
188       pub fn runtime() -> impl RuntimeTrait {
189           RuntimelessRuntime::default()
190       }
191   }
192}
193
194mod with_server;
195pub use with_server::{with_server, with_transport};
196
197mod test_server;
198pub use test_server::{ConnTest, TestServer};
199
200mod runtimeless;
201pub use runtimeless::{RuntimelessClientConfig, RuntimelessRuntime, RuntimelessServer};
202
203/// a sponge Result
204pub type TestResult = Result<(), Box<dyn std::error::Error>>;
205
206/// a test harness for use with [`test_harness`]
207#[track_caller]
208pub fn harness<F, Fut, Output>(test: F) -> Output
209where
210    F: FnOnce() -> Fut,
211    Fut: Future<Output = Output>,
212    Output: Termination,
213{
214    let _ = env_logger::builder().is_test(true).try_init();
215    block_on(test())
216}
217
218/// a harness that includes the runtime
219#[track_caller]
220pub fn with_runtime<F, Fut, Output>(test: F) -> Output
221where
222    F: FnOnce(Runtime) -> Fut,
223    Fut: Future<Output = Output>,
224    Output: Termination,
225{
226    let runtime = runtime();
227    runtime.clone().block_on(test(runtime.into()))
228}
229
230pub use test_harness::test;
231
232mod http_test;
233#[doc(hidden)]
234pub use http_test::HttpTest;
235
236#[cfg(all(feature = "serde_json", feature = "sonic-rs"))]
237compile_error!("cargo features \"serde_json\" and \"sonic-rs\" are mutually exclusive");
238
239#[cfg(feature = "serde_json")]
240#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
241pub use serde_json::{Value, from_str as from_json_str, json, to_string as to_json_string};
242#[cfg(feature = "sonic-rs")]
243#[cfg_attr(docsrs, doc(cfg(feature = "sonic-rs")))]
244pub use sonic_rs::{Value, from_str as from_json_str, json, to_string as to_json_string};