summaryrefslogtreecommitdiffhomepage
path: root/test/test-manager/test_macro/src/lib.rs
blob: 8fe3c329168b2e0c250d0406cdae3434676e57a5 (plain)
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{AttributeArgs, Lit, Meta, NestedMeta, Result};
use test_rpc::meta::Os;

/// Register an `async` function to be run by `test-manager`.
///
/// The `test_function` macro will inject two arguments to your function:
///
/// * `rpc` - a [`test_rpc::client::ServiceClient]` used to make remote-procedure calls inside the
///   virtual machine running the test. This can be used to perform arbitrary network requests,
///   inspect the local file system, rebooting ..
///
/// * `mullvad_client` - a [`mullvad_management_interface::MullvadProxyClient`] which provides a
///   bi-directional communication channel with the `mullvad-daemon` running inside of the virtual
///   machine. All RPC-calls as defined in [`mullvad_management_interface::MullvadProxyClient`] are
///   available on `mullvad_client`.
///
/// # Arguments
///
/// The `test_function` macro takes 3 optional arguments
///
/// * `priority` - The order in which tests will be run where low numbers run before high numbers
///   and tests with the same number run in undefined order. `priority` defaults to 0.
///
/// * `target_os` - The test should only run on the specified OS. This can currently be set to
///   `linux`, `windows`, or `macos`.
///
/// * `skip` - The test should not be run.
///
/// # Examples
///
/// ## Create a standard test.
///
/// Remember that [`test_function`] will inject `rpc` and `mullvad_client` for
/// us.
///
/// ```ignore
/// #[test_function]
/// pub async fn test_function(
///     rpc: ServiceClient,
///     mut mullvad_client: mullvad_management_interface::MullvadProxyClient,
/// ) -> anyhow::Result<()> {
///     Ok(())
/// }
/// ```
///
/// ## Create a test with custom parameters
///
/// This test will run early in the test loop.
///
/// ```ignore
/// #[test_function(priority = -1337)]
/// pub async fn test_function(
///     rpc: ServiceClient,
///     mut mullvad_client: mullvad_management_interface::MullvadProxyClient,
/// ) -> anyhow::Result<()> {
///     Ok(())
/// }
/// ```
#[proc_macro_attribute]
pub fn test_function(attributes: TokenStream, code: TokenStream) -> TokenStream {
    let function: syn::ItemFn = syn::parse(code).unwrap();
    let attributes = syn::parse_macro_input!(attributes as AttributeArgs);

    let test_function = match parse_marked_test_function(&attributes, &function) {
        Ok(tf) => tf,
        Err(e) => return e.into_compile_error().into(),
    };

    let register_test = create_test(test_function);

    quote! {
        #function
        #register_test
    }
    .into_token_stream()
    .into()
}

/// Shorthand for `return syn::Error::new(...)`.
macro_rules! bail {
    ($span:expr, $($tt:tt)*) => {{
        return ::core::result::Result::Err(::syn::Error::new(
            ::syn::spanned::Spanned::span(&$span),
            ::core::format_args!($($tt)*),
        ))
    }};
}

fn parse_marked_test_function(
    attributes: &AttributeArgs,
    function: &syn::ItemFn,
) -> Result<TestFunction> {
    let macro_parameters = get_test_macro_parameters(attributes)?;

    Ok(TestFunction {
        name: function.sig.ident.clone(),
        macro_parameters,
    })
}

fn get_test_macro_parameters(attributes: &syn::AttributeArgs) -> Result<MacroParameters> {
    let mut priority = None;
    let mut targets = vec![];
    let mut skip = false;

    for attribute in attributes {
        match attribute {
            NestedMeta::Meta(Meta::Path(path)) if path.is_ident("skip") => {
                skip = true;
            }
            NestedMeta::Meta(Meta::NameValue(nv)) => {
                let lit = &nv.lit;

                match &nv.path {
                    path if path.is_ident("priority") => match lit {
                        Lit::Int(lit_int) => priority = Some(lit_int.base10_parse().unwrap()),
                        _ => bail!(nv, "'priority' should have an integer value"),
                    },
                    path if path.is_ident("target_os") => {
                        let Lit::Str(lit_str) = lit else {
                            bail!(nv, "'target_os' should have a string value");
                        };

                        let target = match lit_str.value().parse() {
                            Ok(os) => os,
                            Err(e) => bail!(lit_str, "{e}"),
                        };

                        if targets.contains(&target) {
                            bail!(nv, "Duplicate target");
                        }

                        targets.push(target);
                    }
                    _ => bail!(nv, "unknown attribute"),
                }
            }
            _ => bail!(attribute, "unknown attribute"),
        }
    }

    Ok(MacroParameters {
        priority,
        targets,
        skip,
    })
}

fn create_test(test_function: TestFunction) -> proc_macro2::TokenStream {
    let test_function_priority = match test_function.macro_parameters.priority {
        Some(priority) => quote! { Some(#priority) },
        None => quote! { None },
    };
    let targets: proc_macro2::TokenStream = (test_function.macro_parameters.targets.iter())
        .map(|&os| match os {
            Os::Linux => quote! { ::test_rpc::meta::Os::Linux, },
            Os::Macos => quote! { ::test_rpc::meta::Os::Macos, },
            Os::Windows => quote! { ::test_rpc::meta::Os::Windows, },
        })
        .collect();
    let skip = test_function.macro_parameters.skip;

    let func_name = test_function.name;
    let wrapper_closure = quote! {
        |test_context: crate::tests::TestContext,
        rpc: test_rpc::ServiceClient,
        mullvad_client: Option<::mullvad_management_interface::MullvadProxyClient>|
        {
            let mullvad_client = mullvad_client.expect("Test functions defined using the macro should be given a mullvad client");
            Box::pin(async move {
                #func_name(test_context, rpc, mullvad_client).await.map_err(Into::into)
            })
        }
    };

    quote! {
        inventory::submit!(crate::tests::test_metadata::TestMetadata {
            name: stringify!(#func_name),
            targets: &[#targets],
            func: #wrapper_closure,
            priority: #test_function_priority,
            location: None,
            skip: #skip,
        });
    }
}

struct TestFunction {
    name: syn::Ident,
    macro_parameters: MacroParameters,
}

struct MacroParameters {
    priority: Option<i32>,
    targets: Vec<Os>,
    skip: bool,
}