property_test.rs

  1use proc_macro2::TokenStream;
  2use quote::{format_ident, quote, quote_spanned};
  3use syn::{
  4    FnArg, Ident, ItemFn, Type, parse2, punctuated::Punctuated, spanned::Spanned, token::Comma,
  5};
  6
  7pub fn test(args: TokenStream, item: TokenStream) -> TokenStream {
  8    let item_span = item.span();
  9    let Ok(func) = parse2::<ItemFn>(item) else {
 10        return quote_spanned! { item_span =>
 11            compile_error!("#[gpui::property_test] must be placed on a function");
 12        };
 13    };
 14
 15    let test_name = func.sig.ident.clone();
 16    let inner_fn_name = format_ident!("__{test_name}");
 17
 18    let parsed_args = parse_args(func.sig.inputs, &test_name);
 19
 20    let inner_body = func.block;
 21    let inner_arg_decls = parsed_args.inner_fn_decl_args;
 22    let asyncness = func.sig.asyncness;
 23
 24    let inner_fn = quote! {
 25        let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body;
 26    };
 27
 28    let arg_errors = parsed_args.errors;
 29    let proptest_args = parsed_args.proptest_args;
 30    let inner_args = parsed_args.inner_fn_args;
 31    let cx_vars = parsed_args.cx_vars;
 32    let cx_teardowns = parsed_args.cx_teardowns;
 33
 34    let proptest_args = quote! {
 35        #[strategy = ::gpui::seed_strategy()] __seed: u64,
 36        #proptest_args
 37    };
 38
 39    let run_test_body = match &asyncness {
 40        None => quote! {
 41            #cx_vars
 42            #inner_fn_name(#inner_args);
 43            #cx_teardowns
 44        },
 45        Some(_) => quote! {
 46            let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone()));
 47            #cx_vars
 48            foreground_executor.block_test(#inner_fn_name(#inner_args));
 49            #cx_teardowns
 50        },
 51    };
 52
 53    quote! {
 54        #arg_errors
 55
 56        #[::gpui::proptest::property_test(proptest_path = "::gpui::proptest", #args)]
 57        fn #test_name(#proptest_args) {
 58            #inner_fn
 59
 60            ::gpui::run_test_once(
 61                __seed,
 62                Box::new(move |dispatcher| {
 63                    #run_test_body
 64                }),
 65            )
 66        }
 67    }
 68}
 69
 70#[derive(Default)]
 71struct ParsedArgs {
 72    cx_vars: TokenStream,
 73    cx_teardowns: TokenStream,
 74    proptest_args: TokenStream,
 75    errors: TokenStream,
 76
 77    // exprs passed at the call-site
 78    inner_fn_args: TokenStream,
 79    // args in the declaration
 80    inner_fn_decl_args: TokenStream,
 81}
 82
 83fn parse_args(args: Punctuated<FnArg, Comma>, test_name: &Ident) -> ParsedArgs {
 84    let mut parsed = ParsedArgs::default();
 85    let mut args = args.into_iter().collect();
 86
 87    remove_cxs(&mut parsed, &mut args, test_name);
 88    remove_std_rng(&mut parsed, &mut args);
 89    remove_background_executor(&mut parsed, &mut args);
 90
 91    // all remaining args forwarded to proptest's macro
 92    parsed.proptest_args = quote!( #(#args),* );
 93
 94    parsed
 95}
 96
 97fn remove_cxs(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>, test_name: &Ident) {
 98    let mut ix = 0;
 99    args.retain_mut(|arg| {
100        if !is_test_cx(arg) {
101            return true;
102        }
103
104        let cx_varname = format_ident!("cx_{ix}");
105        ix += 1;
106
107        parsed.cx_vars.extend(quote!(
108            let mut #cx_varname = gpui::TestAppContext::build(
109                dispatcher.clone(),
110                Some(stringify!(#test_name)),
111            );
112        ));
113        parsed.cx_teardowns.extend(quote!(
114            dispatcher.run_until_parked();
115            #cx_varname.executor().forbid_parking();
116            #cx_varname.quit();
117            dispatcher.run_until_parked();
118        ));
119
120        parsed.inner_fn_decl_args.extend(quote!(#arg,));
121        parsed.inner_fn_args.extend(quote!(&mut #cx_varname,));
122
123        false
124    });
125}
126
127fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
128    args.retain_mut(|arg| {
129        if !is_std_rng(arg) {
130            return true;
131        }
132
133        parsed.errors.extend(quote_spanned! { arg.span() =>
134            compile_error!("`StdRng` is not allowed in a property test. Consider implementing `Arbitrary`, or implementing a custom `Strategy`. https://altsysrq.github.io/proptest-book/proptest/tutorial/strategy-basics.html");
135        });
136
137        false
138    });
139}
140
141fn remove_background_executor(parsed: &mut ParsedArgs, args: &mut Vec<FnArg>) {
142    args.retain_mut(|arg| {
143        if !is_background_executor(arg) {
144            return true;
145        }
146
147        parsed.inner_fn_decl_args.extend(quote!(#arg,));
148        parsed
149            .inner_fn_args
150            .extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new(
151                dispatcher.clone()
152            )),));
153
154        false
155    });
156}
157
158// Matches `&TestAppContext` or `&foo::bar::baz::TestAppContext`
159fn is_test_cx(arg: &FnArg) -> bool {
160    let FnArg::Typed(arg) = arg else {
161        return false;
162    };
163
164    let Type::Reference(ty) = &*arg.ty else {
165        return false;
166    };
167
168    let Type::Path(ty) = &*ty.elem else {
169        return false;
170    };
171
172    ty.path
173        .segments
174        .last()
175        .is_some_and(|seg| seg.ident == "TestAppContext")
176}
177
178fn is_std_rng(arg: &FnArg) -> bool {
179    is_path_with_last_segment(arg, "StdRng")
180}
181
182fn is_background_executor(arg: &FnArg) -> bool {
183    is_path_with_last_segment(arg, "BackgroundExecutor")
184}
185
186fn is_path_with_last_segment(arg: &FnArg, last_segment: &str) -> bool {
187    let FnArg::Typed(arg) = arg else {
188        return false;
189    };
190
191    let Type::Path(ty) = &*arg.ty else {
192        return false;
193    };
194
195    ty.path
196        .segments
197        .last()
198        .is_some_and(|seg| seg.ident == last_segment)
199}