doco_derive/lib.rs
1//! Derive macros for the Doco testing framework
2//!
3//! Doco is a test runner and library for writing end-to-tests of web applications. It runs tests
4//! in isolated, ephemeral environments. This crate provides procedural macros to make it easier to
5//! set up the test runner, collect all tests, and then run them individually in isolated, ephemeral
6//! environments.
7//!
8//! It is not recommended to use this crate directly. Instead, use the [`doco`] crate that
9//! re-exports the macros from this crate.
10
11use proc_macro::TokenStream;
12use quote::{format_ident, quote};
13use syn::{parse_macro_input, ItemFn};
14
15/// Collect and run the end-to-end tests with Doco
16///
17/// This macro makes it very easy to use the [`doco`] testing framework. It collects all tests that
18/// are annotated with the [`doco::test`] macro, initializes the test runner, and then runs each
19/// test in an isolated, ephemeral environment.
20///
21/// # Example
22///
23/// ```ignore
24/// use doco::{Doco, Server};
25///
26/// #[doco::main]
27/// async fn main() -> Doco {
28/// let server = Server::builder()
29/// .image("crccheck/hello-world")
30/// .tag("v1.0.0")
31/// .port(8000)
32/// .build();
33///
34/// Doco::builder().server(server).build()
35/// }
36/// ```
37#[proc_macro_attribute]
38pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
39 // Parse the function that has been annotated with the `#[doco_derive::main]` attribute
40 let main_fn = parse_macro_input!(input as ItemFn);
41 let main_block = main_fn.block;
42
43 let expanded = quote! {
44 fn main() {
45 doco::TestRunner::new(async #main_block).run();
46 }
47 };
48
49 expanded.into()
50}
51
52/// Annotate an end-to-end test to be run with Doco
53///
54/// The `#[doco::test]` attribute is used to annotate an asynchronous test function that should be
55/// executed by Doco as an end-to-end test. The test function is passed a [`doco::Client`] that can
56/// be used to interact with the web application, and it should return a [`doco::Result`].
57///
58/// # Example
59///
60/// ```ignore
61/// use doco::{Client, Result};
62///
63/// #[doco::test]
64/// async fn visit_root_path(client: Client) -> Result<()> {
65/// client.goto("/").await?;
66///
67/// let body = client.source().await?;
68///
69/// assert!(body.contains("Hello World"));
70///
71/// Ok(())
72/// }
73/// ```
74#[proc_macro_attribute]
75pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
76 // Parse the function that has been annotated with the `#[doco_derive::test]` attribute
77 let input_fn = parse_macro_input!(input as ItemFn);
78 let input_fn_ident = &input_fn.sig.ident;
79 let input_fn_name = input_fn_ident.to_string();
80
81 // Extract the function name, arguments, and body for the final test function
82 let test_fn_ident = format_ident!("{}_test", &input_fn_ident);
83 let test_args = &input_fn.sig.inputs;
84
85 // Generate a test function that executes the test block inside doco's asynchronous runtime
86 let test_function = quote! {
87 #input_fn
88
89 fn #test_fn_ident(#test_args) -> doco::Result<()> {
90 std::thread::spawn(move || {
91 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
92
93 runtime.block_on(async {
94 #input_fn_ident(client).await
95 })
96 })
97 .join().map_err(|_| doco::anyhow!("failed to run test in isolated thread"))?
98 }
99
100 doco::inventory::submit!(doco::TestCase {
101 name: #input_fn_name,
102 function: #test_fn_ident
103 });
104 };
105
106 test_function.into()
107}