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}