Skip to main content

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}