util_macros.rs

  1#![allow(clippy::test_attr_in_doctest)]
  2
  3use convert_case::{Case, Casing};
  4use perf::*;
  5use proc_macro::TokenStream;
  6use proc_macro2::TokenStream as TokenStream2;
  7use quote::{ToTokens, format_ident, quote};
  8use syn::{
  9    Data, DeriveInput, Expr, ExprArray, ExprLit, Fields, ItemFn, Lit, LitStr, MetaNameValue, Token,
 10    parse_macro_input, parse_quote, punctuated::Punctuated,
 11};
 12
 13/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
 14/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
 15/// returned unmodified.
 16///
 17/// # Example
 18/// ```rust
 19/// use util_macros::path;
 20///
 21/// let path = path!("/Users/user/file.txt");
 22/// #[cfg(target_os = "windows")]
 23/// assert_eq!(path, "C:\\Users\\user\\file.txt");
 24/// #[cfg(not(target_os = "windows"))]
 25/// assert_eq!(path, "/Users/user/file.txt");
 26/// ```
 27#[proc_macro]
 28pub fn path(input: TokenStream) -> TokenStream {
 29    let path = parse_macro_input!(input as LitStr);
 30
 31    #[cfg(target_os = "windows")]
 32    {
 33        let mut path = path.value();
 34        path = path.replace("/", "\\");
 35        if path.starts_with("\\") {
 36            path = format!("C:{}", path);
 37        }
 38        return TokenStream::from(quote! {
 39            #path
 40        });
 41    }
 42
 43    #[cfg(not(target_os = "windows"))]
 44    {
 45        let path = path.value();
 46        return TokenStream::from(quote! {
 47            #path
 48        });
 49    }
 50}
 51
 52/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
 53/// But if the target OS is not Windows, the URI is returned as is.
 54///
 55/// # Example
 56/// ```rust
 57/// use util_macros::uri;
 58///
 59/// let uri = uri!("file:///path/to/file");
 60/// #[cfg(target_os = "windows")]
 61/// assert_eq!(uri, "file:///C:/path/to/file");
 62/// #[cfg(not(target_os = "windows"))]
 63/// assert_eq!(uri, "file:///path/to/file");
 64/// ```
 65#[proc_macro]
 66pub fn uri(input: TokenStream) -> TokenStream {
 67    let uri = parse_macro_input!(input as LitStr);
 68    let uri = uri.value();
 69
 70    #[cfg(target_os = "windows")]
 71    let uri = uri.replace("file:///", "file:///C:/");
 72
 73    TokenStream::from(quote! {
 74        #uri
 75    })
 76}
 77
 78/// This macro replaces the line endings `\n` with `\r\n` for Windows.
 79/// But if the target OS is not Windows, the line endings are returned as is.
 80///
 81/// # Example
 82/// ```rust
 83/// use util_macros::line_endings;
 84///
 85/// let text = line_endings!("Hello\nWorld");
 86/// #[cfg(target_os = "windows")]
 87/// assert_eq!(text, "Hello\r\nWorld");
 88/// #[cfg(not(target_os = "windows"))]
 89/// assert_eq!(text, "Hello\nWorld");
 90/// ```
 91#[proc_macro]
 92pub fn line_endings(input: TokenStream) -> TokenStream {
 93    let text = parse_macro_input!(input as LitStr);
 94    let text = text.value();
 95
 96    #[cfg(target_os = "windows")]
 97    let text = text.replace("\n", "\r\n");
 98
 99    TokenStream::from(quote! {
100        #text
101    })
102}
103
104/// Inner data for the perf macro.
105#[derive(Default)]
106struct PerfArgs {
107    /// How many times to loop a test before rerunning the test binary. If left
108    /// empty, the test harness will auto-determine this value.
109    iterations: Option<syn::Expr>,
110    /// How much this test's results should be weighed when comparing across runs.
111    /// If unspecified, defaults to `WEIGHT_DEFAULT` (50).
112    weight: Option<syn::Expr>,
113    /// How relevant a benchmark is to overall performance. See docs on the enum
114    /// for details. If unspecified, `Average` is selected.
115    importance: Importance,
116}
117
118#[warn(clippy::all, clippy::pedantic)]
119impl PerfArgs {
120    /// Parses attribute arguments into a `PerfArgs`.
121    fn parse_into(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
122        if meta.path.is_ident("iterations") {
123            self.iterations = Some(meta.value()?.parse()?);
124        } else if meta.path.is_ident("weight") {
125            self.weight = Some(meta.value()?.parse()?);
126        } else if meta.path.is_ident("critical") {
127            self.importance = Importance::Critical;
128        } else if meta.path.is_ident("important") {
129            self.importance = Importance::Important;
130        } else if meta.path.is_ident("average") {
131            // This shouldn't be specified manually, but oh well.
132            self.importance = Importance::Average;
133        } else if meta.path.is_ident("iffy") {
134            self.importance = Importance::Iffy;
135        } else if meta.path.is_ident("fluff") {
136            self.importance = Importance::Fluff;
137        } else {
138            return Err(syn::Error::new_spanned(meta.path, "unexpected identifier"));
139        }
140        Ok(())
141    }
142}
143
144/// Marks a test as perf-sensitive, to be triaged when checking the performance
145/// of a build. This also automatically applies `#[test]`.
146///
147///
148/// # Usage
149/// Applying this attribute to a test marks it as average importance by default.
150/// There are 4 levels of importance (`Critical`, `Important`, `Average`, `Fluff`);
151/// see the documentation on `Importance` for details. Add the importance as a
152/// parameter to override the default (e.g. `#[perf(important)]`).
153///
154/// Each test also has a weight factor. This is irrelevant on its own, but is considered
155/// when comparing results across different runs. By default, this is set to 50;
156/// pass `weight = n` as a parameter to override this. Note that this value is only
157/// relevant within its importance category.
158///
159/// By default, the number of iterations when profiling this test is auto-determined.
160/// If this needs to be overwritten, pass the desired iteration count as a parameter
161/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test
162/// an arbitrary number times; this flag just sets the number of executions before the
163/// process is restarted and global state is reset.
164///
165/// This attribute should probably not be applied to tests that do any significant
166/// disk IO, as locks on files may not be released in time when repeating a test many
167/// times. This might lead to spurious failures.
168///
169/// # Examples
170/// ```rust
171/// use util_macros::perf;
172///
173/// #[perf]
174/// fn generic_test() {
175///     // Test goes here.
176/// }
177///
178/// #[perf(fluff, weight = 30)]
179/// fn cold_path_test() {
180///     // Test goes here.
181/// }
182/// ```
183///
184/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
185/// be used with automatic iterations.
186/// ```rust,ignore
187/// use util_macros::perf;
188///
189/// #[perf(iterations = 1, critical)]
190/// #[gpui::test]
191/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
192///     // Test goes here.
193/// }
194/// ```
195#[proc_macro_attribute]
196#[warn(clippy::all, clippy::pedantic)]
197pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
198    let mut args = PerfArgs::default();
199    let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta));
200    parse_macro_input!(our_attr with parser);
201
202    let ItemFn {
203        attrs: mut attrs_main,
204        vis,
205        sig: mut sig_main,
206        block,
207    } = parse_macro_input!(input as ItemFn);
208    attrs_main.push(parse_quote!(#[test]));
209    attrs_main.push(parse_quote!(#[allow(non_snake_case)]));
210
211    let fns = if cfg!(perf_enabled) {
212        #[allow(clippy::wildcard_imports, reason = "We control the other side")]
213        use consts::*;
214
215        // Make the ident obvious when calling, for the test parser.
216        // Also set up values for the second metadata-returning "test".
217        let mut new_ident_main = sig_main.ident.to_string();
218        let mut new_ident_meta = new_ident_main.clone();
219        new_ident_main.push_str(SUF_NORMAL);
220        new_ident_meta.push_str(SUF_MDATA);
221
222        let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span());
223        sig_main.ident = new_ident_main;
224
225        // We don't want any nonsense if the original test had a weird signature.
226        let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span());
227        let sig_meta = parse_quote!(fn #new_ident_meta());
228        let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]);
229
230        // Make the test loop as the harness instructs it to.
231        let block_main = {
232            // The perf harness will pass us the value in an env var. Even if we
233            // have a preset value, just do this to keep the code paths unified.
234            parse_quote!({
235                let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
236                for _ in 0..iter_count {
237                    #block
238                }
239            })
240        };
241        let importance = format!("{}", args.importance);
242        let block_meta = {
243            // This function's job is to just print some relevant info to stdout,
244            // based on the params this attr is passed. It's not an actual test.
245            // Since we use a custom attr set on our metadata fn, it shouldn't
246            // cause problems with xfail tests.
247            let q_iter = if let Some(iter) = args.iterations {
248                quote! {
249                    println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter);
250                }
251            } else {
252                quote! {}
253            };
254            let weight = args
255                .weight
256                .unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT });
257            parse_quote!({
258                #q_iter
259                println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight);
260                println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance);
261                println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER);
262            })
263        };
264
265        vec![
266            // The real test.
267            ItemFn {
268                attrs: attrs_main,
269                vis: vis.clone(),
270                sig: sig_main,
271                block: block_main,
272            },
273            // The fake test.
274            ItemFn {
275                attrs: attrs_meta,
276                vis,
277                sig: sig_meta,
278                block: block_meta,
279            },
280        ]
281    } else {
282        vec![ItemFn {
283            attrs: attrs_main,
284            vis,
285            sig: sig_main,
286            block,
287        }]
288    };
289
290    fns.into_iter()
291        .flat_map(|f| TokenStream::from(f.into_token_stream()))
292        .collect()
293}
294
295#[proc_macro_derive(FieldAccessByEnum, attributes(field_access_by_enum))]
296pub fn derive_field_access_by_enum(input: TokenStream) -> TokenStream {
297    let input = parse_macro_input!(input as DeriveInput);
298
299    let struct_name = &input.ident;
300
301    let mut enum_name = None;
302    let mut enum_attrs: Vec<TokenStream2> = Vec::new();
303
304    for attr in &input.attrs {
305        if attr.path().is_ident("field_access_by_enum") {
306            let name_values: Punctuated<MetaNameValue, Token![,]> =
307                attr.parse_args_with(Punctuated::parse_terminated).unwrap();
308            for name_value in name_values {
309                if name_value.path.is_ident("enum_name") {
310                    let value = name_value.value;
311                    match value {
312                        Expr::Lit(ExprLit {
313                            lit: Lit::Str(name),
314                            ..
315                        }) => enum_name = Some(name.value()),
316                        _ => panic!("Expected string literal in enum_name attribute"),
317                    }
318                } else if name_value.path.is_ident("enum_attrs") {
319                    let value = name_value.value;
320                    match value {
321                        Expr::Array(ExprArray { elems, .. }) => {
322                            for elem in elems {
323                                enum_attrs.push(quote!(#[#elem]));
324                            }
325                        }
326                        _ => panic!("Expected array literal in enum_attr attribute"),
327                    }
328                } else {
329                    if let Some(ident) = name_value.path.get_ident() {
330                        panic!("Unrecognized argument name {}", ident);
331                    } else {
332                        panic!("Unrecognized argument {:?}", name_value.path);
333                    }
334                }
335            }
336        }
337    }
338    let Some(enum_name) = enum_name else {
339        panic!("#[field_access_by_enum(enum_name = \"...\")] attribute is required");
340    };
341    let enum_ident = format_ident!("{}", enum_name);
342
343    let fields = match input.data {
344        Data::Struct(data_struct) => match data_struct.fields {
345            Fields::Named(fields) => fields.named,
346            _ => panic!("FieldAccessByEnum can only be derived for structs with named fields"),
347        },
348        _ => panic!("FieldAccessByEnum can only be derived for structs"),
349    };
350
351    if fields.is_empty() {
352        panic!("FieldAccessByEnum cannot be derived for structs with no fields");
353    }
354
355    let mut enum_variants = Vec::new();
356    let mut get_match_arms = Vec::new();
357    let mut set_match_arms = Vec::new();
358    let mut field_types = Vec::new();
359
360    for field in fields.iter() {
361        let field_name = field.ident.as_ref().unwrap();
362        let variant_name = field_name.to_string().to_case(Case::Pascal);
363        let variant_ident = format_ident!("{}", variant_name);
364        let field_type = &field.ty;
365
366        enum_variants.push(variant_ident.clone());
367        field_types.push(field_type);
368
369        get_match_arms.push(quote! {
370            #enum_ident::#variant_ident => &self.#field_name,
371        });
372
373        set_match_arms.push(quote! {
374            #enum_ident::#variant_ident => self.#field_name = value,
375        });
376    }
377
378    let first_type = &field_types[0];
379    let all_same_type = field_types
380        .iter()
381        .all(|ty| quote!(#ty).to_string() == quote!(#first_type).to_string());
382    if !all_same_type {
383        panic!("Fields have different types.");
384    }
385    let field_value_type = quote! { #first_type };
386
387    let expanded = quote! {
388        #(#enum_attrs)*
389        pub enum #enum_ident {
390            #(#enum_variants),*
391        }
392
393        impl util::FieldAccessByEnum<#field_value_type> for #struct_name {
394            type Field = #enum_ident;
395
396            fn get_field_by_enum(&self, field: Self::Field) -> &#field_value_type {
397                match field {
398                    #(#get_match_arms)*
399                }
400            }
401
402            fn set_field_by_enum(&mut self, field: Self::Field, value: #field_value_type) {
403                match field {
404                    #(#set_match_arms)*
405                }
406            }
407        }
408    };
409
410    TokenStream::from(expanded)
411}