trdelnik_test/
lib.rs

1//! The `trdelnik_test` crate helps you to write Rust tests for your _programs_ with Trdelnik.
2//! See the macro [trdelnik_test] for more info.
3//!
4//! _Dev Note_: You need `cargo expand` and nightly Rust to run tests. See [macrotest docs](https://docs.rs/macrotest/latest/macrotest/#workflow).
5
6use darling::FromMeta;
7use proc_macro::TokenStream;
8use syn::{parse_macro_input, spanned::Spanned, AttributeArgs, ItemFn};
9
10#[derive(Debug, FromMeta)]
11struct MacroArgs {
12    #[darling(default)]
13    root: Option<String>,
14}
15
16/// The macro starts the Solana validator (localnet), runs your program test and then shuts down the validator.
17/// - The test implicitly returns [anyhow::Result<()>](https://docs.rs/anyhow/latest/anyhow/type.Result.html).
18/// - All tests are run sequentially - each test uses a new/reset validator. (See [serial_test::serial](https://docs.rs/serial_test/latest/serial_test/attr.serial.html))
19/// - Async support is provided by Tokio: [tokio::test(flavor = "multi_thread")](https://docs.rs/tokio/latest/tokio/attr.test.html).
20/// - The macro accepts one optional argument `root` with the default value `"../../"`.
21///      - Example: `#[trdelnik_test(root = "../../")]`
22/// - You can see the macro expanded in the crate's tests.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// // tests/test.rs
28/// use trdelnik_client::*;
29///
30/// #[trdelnik_test]
31/// async fn test_turnstile() {
32///     let reader = Reader::new();
33///     let mut turnstile = Turnstile {
34///         client: Client::new(reader.keypair("id").await?),
35///         state: reader.keypair("state").await?,
36///         program: reader.keypair("program").await?,
37///         program_data: reader.program_data("turnstile").await?,
38///         locked: bool::default(),
39///     };
40///     turnstile.initialize().await?;
41/// }
42/// ```
43#[proc_macro_attribute]
44pub fn trdelnik_test(args: TokenStream, input: TokenStream) -> TokenStream {
45    let attr_args = parse_macro_input!(args as AttributeArgs);
46    let macro_args = match MacroArgs::from_list(&attr_args) {
47        Ok(macro_args) => macro_args,
48        Err(error) => {
49            return TokenStream::from(error.write_errors());
50        }
51    };
52    let root = macro_args.root.unwrap_or_else(|| "../../".to_owned());
53
54    let input_fn: ItemFn =
55        syn::parse(input).expect("'trdelnik_test' attribute is applicable only to async fn");
56
57    let input_fn_span = input_fn.span();
58    let input_fn_body = input_fn.block;
59    let input_fn_name = input_fn.sig.ident;
60    let input_fn_attrs = input_fn.attrs;
61    let input_fn_inputs = input_fn.sig.inputs;
62
63    quote::quote_spanned!(input_fn_span=>
64        #(#input_fn_attrs)*
65        // Note: The line `#(#input_fn_attrs)*` has to be above the line with the code
66        // `#[trdelnik_client::tokio::test...` to make macros like `#[rstest]` work -
67        // see https://github.com/la10736/rstest#inject-test-attribute
68        #[trdelnik_client::rstest]
69        #[trdelnik_client::tokio::test(flavor = "multi_thread")]
70        #[trdelnik_client::serial_test::serial]
71        async fn #input_fn_name(#input_fn_inputs) -> trdelnik_client::anyhow::Result<()> {
72            let mut tester = trdelnik_client::Tester::with_root(#root);
73            let localnet_handle = tester.before().await?;
74            let test = async {
75                #input_fn_body
76                Ok::<(), trdelnik_client::anyhow::Error>(())
77            };
78            let result = std::panic::AssertUnwindSafe(test).catch_unwind().await;
79            tester.after(localnet_handle).await?;
80            assert!(result.is_ok());
81            let final_result = result.unwrap();
82            if let Err(error) = final_result {
83                trdelnik_client::error_reporter::report_error(&error);
84                return Err(error);
85            }
86            Ok(())
87        }
88    )
89    .into()
90}