doco_derive/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! Derive macros for the Doco testing framework
//!
//! Doco is a test runner and library for writing end-to-tests of web applications. It runs tests
//! in isolated, ephemeral environments. This crate provides procedural macros to make it easier to
//! set up the test runner, collect all tests, and then run them individually in isolated, ephemeral
//! environments.
//!
//! It is not recommended to use this crate directly. Instead, use the [`doco`] crate that
//! re-exports the macros from this crate.

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, ItemFn};

/// Collect and run the end-to-end tests with Doco
///
/// This macro makes it very easy to use the [`doco`] testing framework. It collects all tests that
/// are annotated with the [`doco::test`] macro, initializes the test runner, and then runs each
/// test in an isolated, ephemeral environment.
///
/// # Example
///
/// ```ignore
/// use doco::{Doco, Server};
///
/// #[doco::main]
/// async fn main() -> Doco {
///     let server = Server::builder()
///         .image("crccheck/hello-world")
///         .tag("v1.0.0")
///         .port(8000)
///         .build();
///
///     Doco::builder().server(server).build()
/// }
/// ```
#[proc_macro_attribute]
pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
    // Parse the function that has been annotated with the `#[doco_derive::main]` attribute
    let main_fn = parse_macro_input!(input as ItemFn);
    let main_block = main_fn.block;

    // Generate code that initializes the asynchronous runtime, the inventory for tests, and then
    // sets up the given function as the entry point for the program
    let initialization_and_function = quote! {
        #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
        struct TestCase {
            pub name: &'static str,
            pub function: fn(doco::Client) -> doco::Result<()>,
        }

        doco::inventory::collect!(TestCase);

        #[tokio::main]
        async fn main() {
            let doco: doco::Doco = #main_block;

            let test_runner = doco::TestRunner::init(doco).await.expect("failed to initialize the test runner");
            let tests = doco::inventory::iter::<TestCase>.into_iter().count();

            println!("Running {} tests...\n", tests);

            for test in doco::inventory::iter::<TestCase> {
                // TODO: Collect results, report them, and remove the `expect` statement
                test_runner.run(test.name, test.function).await.expect("failed to run test");
            }

            println!("\nDone.");
        }
    };

    initialization_and_function.into()
}

/// Annotate an end-to-end test to be run with Doco
///
/// The `#[doco::test]` attribute is used to annotate an asynchronous test function that should be
/// executed by Doco as an end-to-end test. The test function is passed a [`doco::Client`] that can
/// be used to interact with the web application, and it should return a [`doco::Result`].
///
/// # Example
///
/// ```ignore
/// use doco::{Client, Result};
///
/// #[doco::test]
/// async fn visit_root_path(client: Client) -> Result<()> {
///     client.goto("/").await?;
///
///     let body = client.source().await?;
///
///     assert!(body.contains("Hello World"));
///
///     Ok(())
/// }
/// ```
#[proc_macro_attribute]
pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
    // Parse the function that has been annotated with the `#[doco_derive::test]` attribute
    let input_fn = parse_macro_input!(input as ItemFn);
    let input_fn_ident = &input_fn.sig.ident;
    let input_fn_name = input_fn_ident.to_string();

    // Extract the function name, arguments, and body for the final test function
    let test_fn_ident = format_ident!("{}_test", &input_fn_ident);
    let test_args = &input_fn.sig.inputs;

    // Generate a test function that executes the test block inside doco's asynchronous runtime
    let test_function = quote! {
        #input_fn

        fn #test_fn_ident(#test_args) -> doco::Result<()> {
            std::thread::spawn(move || {
                let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;

                runtime.block_on(async {
                    #input_fn_ident(client).await
                })
            })
            .join().map_err(|_| doco::anyhow!("failed to run test in isolated thread"))?
        }

        doco::inventory::submit!(crate::TestCase {
            name: #input_fn_name,
            function: #test_fn_ident
        });
    };

    test_function.into()
}