trillium_testing/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    clippy::dbg_macro,
4    missing_copy_implementations,
5    rustdoc::missing_crate_level_docs,
6    missing_debug_implementations,
7    missing_docs,
8    nonstandard_style,
9    unused_qualifications
10)]
11
12/*!
13testing utilities for trillium applications.
14
15this crate is intended to be used as a development dependency.
16
17```
18use trillium_testing::prelude::*;
19use trillium::{Conn, conn_try};
20async fn handler(mut conn: Conn) -> Conn {
21    let request_body = conn_try!(conn.request_body_string().await, conn);
22    conn.with_body(format!("request body was: {}", request_body))
23        .with_status(418)
24        .with_response_header("request-id", "special-request")
25}
26
27assert_response!(
28    post("/").with_request_body("hello trillium!").on(&handler),
29    Status::ImATeapot,
30    "request body was: hello trillium!",
31    "request-id" => "special-request",
32    "content-length" => "33"
33);
34
35```
36
37## Features
38
39**You must enable a runtime feature for trillium testing**
40
41### Tokio:
42```toml
43[dev-dependencies]
44# ...
45trillium-testing = { version = "0.2", features = ["tokio"] }
46```
47
48### Async-std:
49```toml
50[dev-dependencies]
51# ...
52trillium-testing = { version = "0.2", features = ["async-std"] }
53```
54
55### Smol:
56```toml
57[dev-dependencies]
58# ...
59trillium-testing = { version = "0.2", features = ["smol"] }
60```
61
62
63*/
64
65mod assertions;
66
67mod test_transport;
68use std::future::{Future, IntoFuture};
69use std::process::Termination;
70
71pub use test_transport::TestTransport;
72
73mod test_conn;
74pub use test_conn::TestConn;
75
76pub mod methods;
77pub mod prelude {
78    /*!
79    useful stuff for testing trillium apps
80    */
81    pub use crate::{
82        assert_body, assert_body_contains, assert_headers, assert_not_handled, assert_ok,
83        assert_response, assert_status, block_on, connector, init, methods::*,
84    };
85
86    pub use trillium::{Conn, Method, Status};
87}
88
89pub use trillium::{Method, Status};
90
91pub use url::Url;
92
93/// initialize a handler
94pub fn init(handler: &mut impl trillium::Handler) {
95    let mut info = "testing".into();
96    block_on(handler.init(&mut info))
97}
98
99// these exports are used by macros
100pub use futures_lite;
101pub use futures_lite::{AsyncRead, AsyncReadExt, AsyncWrite};
102
103mod server_connector;
104pub use server_connector::{connector, ServerConnector};
105
106use trillium_server_common::Config;
107pub use trillium_server_common::{Connector, ObjectSafeConnector, Server, ServerHandle};
108
109#[derive(Debug)]
110/// A droppable future
111///
112/// This only exists because of the #[must_use] on futures. The task will run to completion whether
113/// or not this future is awaited.
114pub struct SpawnHandle<F>(F);
115impl<F> IntoFuture for SpawnHandle<F>
116where
117    F: Future,
118{
119    type IntoFuture = F;
120    type Output = F::Output;
121    fn into_future(self) -> Self::IntoFuture {
122        self.0
123    }
124}
125
126cfg_if::cfg_if! {
127    if #[cfg(feature = "smol")] {
128        /// runtime server config
129        pub fn config() -> Config<impl Server, ()> {
130            trillium_smol::config()
131        }
132
133        /// smol-based spawn variant that finishes whether or not the returned future is dropped
134        pub fn spawn<Fut, Out>(future: Fut) -> SpawnHandle<impl Future<Output = Option<Out>>>
135        where
136            Fut: Future<Output = Out> + Send + 'static,
137            Out: Send + 'static
138        {
139            let (tx, rx) = async_channel::bounded::<Out>(1);
140            trillium_smol::async_global_executor::spawn(async move { let _ = tx.send(future.await).await; }).detach();
141            SpawnHandle(async move {
142                let rx = rx;
143                rx.recv().await.ok()
144            })
145        }
146
147        /// runtime client config
148        pub fn client_config() -> impl Connector {
149            ClientConfig::default()
150        }
151        pub use trillium_smol::async_global_executor::block_on;
152        pub use trillium_smol::ClientConfig;
153
154    } else if #[cfg(feature = "async-std")] {
155        /// runtime server config
156        pub fn config() -> Config<impl Server, ()> {
157            trillium_async_std::config()
158        }
159        pub use trillium_async_std::async_std::task::block_on;
160        pub use trillium_async_std::ClientConfig;
161
162        /// async-std-based spawn variant that finishes whether or not the returned future is dropped
163        pub fn spawn<Fut, Out>(future: Fut) -> SpawnHandle<impl Future<Output = Option<Out>>>
164        where
165            Fut: Future<Output = Out> + Send + 'static,
166            Out: Send + 'static
167        {
168            let (tx, rx) = async_channel::bounded::<Out>(1);
169            trillium_async_std::async_std::task::spawn(async move { let _ = tx.send(future.await).await; });
170            SpawnHandle(async move {
171                let rx = rx;
172                rx.recv().await.ok()
173            })
174        }
175        /// runtime client config
176        pub fn client_config() -> impl Connector {
177            ClientConfig::default()
178        }
179    } else if #[cfg(feature = "tokio")] {
180        /// runtime server config
181        pub fn config() -> Config<impl Server, ()> {
182            trillium_tokio::config()
183        }
184        pub use trillium_tokio::ClientConfig;
185        pub use trillium_tokio::block_on;
186        /// tokio-based spawn variant that finishes whether or not the returned future is dropped
187        pub fn spawn<Fut, Out>(future: Fut) -> SpawnHandle<impl Future<Output = Option<Out>>>
188        where
189            Fut: Future<Output = Out> + Send + 'static,
190            Out: Send + 'static
191        {
192            let (tx, rx) = async_channel::bounded::<Out>(1);
193            trillium_tokio::tokio::task::spawn(async move { let _ = tx.send(future.await).await; });
194            SpawnHandle(async move {
195                let rx = rx;
196                rx.recv().await.ok()
197            })
198        }
199        /// runtime client config
200        pub fn client_config() -> impl Connector {
201            ClientConfig::default()
202        }
203   } else {
204        /// runtime server config
205        pub fn config() -> Config<impl Server, ()> {
206            Config::<RuntimelessServer, ()>::new()
207        }
208
209        pub use RuntimelessClientConfig as ClientConfig;
210
211        /// generic client config
212        pub fn client_config() -> impl Connector {
213            RuntimelessClientConfig::default()
214        }
215
216        pub use futures_lite::future::block_on;
217
218        /// fake runtimeless spawn that finishes whether or not the future is dropped
219        pub fn spawn<Fut, Out>(future: Fut) -> SpawnHandle<impl Future<Output = Option<Out>>>
220        where
221            Fut: Future<Output = Out> + Send + 'static,
222            Out: Send + 'static
223        {
224            let (tx, rx) = async_channel::bounded::<Out>(1);
225            std::thread::spawn(move || { let _ = tx.send_blocking(block_on(future)); });
226            SpawnHandle(async move {
227                let rx = rx;
228                rx.recv().await.ok()
229            })
230        }
231    }
232}
233
234mod with_server;
235pub use with_server::{with_server, with_transport};
236
237mod runtimeless;
238pub use runtimeless::{RuntimelessClientConfig, RuntimelessServer};
239
240/// a sponge Result
241pub type TestResult = Result<(), Box<dyn std::error::Error>>;
242
243/// a test harness for use with [`test_harness`]
244#[track_caller]
245pub fn harness<F, Fut, Output>(test: F) -> Output
246where
247    F: FnOnce() -> Fut,
248    Fut: Future<Output = Output>,
249    Output: Termination,
250{
251    block_on(test())
252}