assay_proc_macro/
lib.rs

1/*
2 * Copyright (C) 2021 Michael Gattozzi <self@mgattozzi.dev>
3 *
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7 */
8
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{
12  parse::{Parse, ParseStream},
13  parse_macro_input, Expr, ExprArray, ExprLit, ExprTuple, Ident, ItemFn, Lit, Result, Token,
14};
15
16struct AssayAttribute {
17  include: Option<Vec<String>>,
18  should_panic: bool,
19  env: Option<Vec<(String, String)>>,
20  setup: Option<Expr>,
21  teardown: Option<Expr>,
22}
23
24impl Parse for AssayAttribute {
25  fn parse(input: ParseStream) -> Result<Self> {
26    let mut include = None;
27    let mut should_panic = false;
28    let mut env = None;
29    let mut setup = None;
30    let mut teardown = None;
31
32    while input.peek(Ident) || {
33      if input.peek(Token![,]) {
34        let _: Token![,] = input.parse()?;
35      }
36      input.peek(Ident)
37    } {
38      let ident: Ident = input.parse()?;
39      match ident.to_string().as_str() {
40        "include" => {
41          let _: Token![=] = input.parse()?;
42          let array: ExprArray = input.parse()?;
43          include = Some(
44            array
45              .elems
46              .into_iter()
47              .filter_map(|e| match e {
48                Expr::Lit(ExprLit {
49                  lit: Lit::Str(lit_str),
50                  ..
51                }) => Some(lit_str.value()),
52                _ => None,
53              })
54              .collect(),
55          );
56        }
57        "should_panic" => should_panic = true,
58        "env" => {
59          let _: Token![=] = input.parse()?;
60          let array: ExprArray = input.parse()?;
61          env = Some(
62            array
63              .elems
64              .into_iter()
65              .filter_map(|e| match e {
66                Expr::Tuple(ExprTuple { elems, .. }) => match (&elems[0], &elems[1]) {
67                  (
68                    Expr::Lit(ExprLit {
69                      lit: Lit::Str(lit_1),
70                      ..
71                    }),
72                    Expr::Lit(ExprLit {
73                      lit: Lit::Str(lit_2),
74                      ..
75                    }),
76                  ) => Some((lit_1.value(), lit_2.value())),
77                  _ => None,
78                },
79                _ => None,
80              })
81              .collect(),
82          );
83        }
84        val @ "setup" | val @ "teardown" => {
85          let _: Token![=] = input.parse()?;
86          let x = input.parse()?;
87          if val == "setup" {
88            setup = Some(x);
89          } else {
90            teardown = Some(x);
91          }
92        }
93        _ => {}
94      }
95    }
96
97    Ok(AssayAttribute {
98      include,
99      should_panic,
100      env,
101      setup,
102      teardown,
103    })
104  }
105}
106
107#[proc_macro_attribute]
108pub fn assay(attr: TokenStream, item: TokenStream) -> TokenStream {
109  let attr = parse_macro_input!(attr as AssayAttribute);
110
111  let include = if let Some(include) = attr.include {
112    let mut out = quote! {
113      let fs = assay::PrivateFS::new()?;
114    };
115    for file in include {
116      out = quote! {
117        #out
118        fs.include(#file)?;
119      };
120    }
121    out
122  } else {
123    quote! {
124      let fs = assay::PrivateFS::new()?;
125    }
126  };
127
128  let should_panic = if attr.should_panic {
129    quote! { #[should_panic] }
130  } else {
131    quote! {}
132  };
133
134  let env = if let Some(env) = attr.env {
135    let mut out = quote! {};
136    for (k, v) in env {
137      out = quote! {
138        #out
139        std::env::set_var(#k,#v);
140      };
141    }
142    out
143  } else {
144    quote! {}
145  };
146
147  let setup = match attr.setup {
148    Some(expr) => quote! { #expr; },
149    None => quote! {},
150  };
151  let teardown = match attr.teardown {
152    Some(expr) => quote! { #expr; },
153    None => quote! {},
154  };
155
156  // Parse the function out into individual parts
157  let func = parse_macro_input!(item as ItemFn);
158  let vis = func.vis;
159  let mut sig = func.sig;
160  let name = sig.ident.clone();
161  let asyncness = sig.asyncness.take();
162  let block = func.block;
163  let body = if asyncness.is_some() {
164    quote! {
165      async fn inner_async() -> Result<(), Box<dyn std::error::Error>> {
166        #block
167        Ok(())
168      }
169      assay::Runtime::new()?.block_on(inner_async())?;
170    }
171  } else {
172    quote! { #block }
173  };
174
175  let expanded = quote! {
176      #[test]
177      #should_panic
178      #vis #sig {
179        fn modify(_: &mut std::process::Command) {}
180
181        fn parent(child: &mut assay::ChildWrapper, _: &mut std::fs::File) {
182          let child = child.wait().unwrap();
183          if !child.success() {
184              panic!("Assay test failed")
185          }
186        }
187
188        fn child() {
189          #[allow(unreachable_code)]
190          if let Err(e) = || -> Result<(), Box<dyn std::error::Error>> {
191            use assay::{assert_eq, assert_ne};
192            #include
193            #setup
194            #env
195            #body
196            #teardown
197            Ok(())
198          }() {
199            panic!("Error: {}", e);
200          }
201        }
202
203      if std::env::var("NEXTEST")
204        .ok()
205        .as_ref()
206        .map(|s| s.as_str() == "1")
207        .unwrap_or(false)
208      {
209        child();
210      } else {
211        let name = {
212          let mut module = module_path!()
213            .split("::")
214            .into_iter()
215            .skip(1)
216            .collect::<Vec<_>>();
217          module.push(stringify!(#name));
218          module.join("::")
219        };
220
221        assay::fork(
222            &name,
223            assay::rusty_fork_id!(),
224            modify,
225            parent,
226            child
227        ).expect("We forked the test using assay");
228      }
229    }
230  };
231
232  // Hand the output tokens back to the compiler.
233  TokenStream::from(expanded)
234}