util_macros.rs

  1#![cfg_attr(not(target_os = "windows"), allow(unused))]
  2#![allow(clippy::test_attr_in_doctest)]
  3
  4use perf::*;
  5use proc_macro::TokenStream;
  6use quote::{ToTokens, quote};
  7use syn::{ItemFn, LitStr, parse_macro_input, parse_quote};
  8
  9/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
 10/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
 11/// returned unmodified.
 12///
 13/// # Example
 14/// ```rust
 15/// use util_macros::path;
 16///
 17/// let path = path!("/Users/user/file.txt");
 18/// #[cfg(target_os = "windows")]
 19/// assert_eq!(path, "C:\\Users\\user\\file.txt");
 20/// #[cfg(not(target_os = "windows"))]
 21/// assert_eq!(path, "/Users/user/file.txt");
 22/// ```
 23#[proc_macro]
 24pub fn path(input: TokenStream) -> TokenStream {
 25    let path = parse_macro_input!(input as LitStr);
 26    let mut path = path.value();
 27
 28    #[cfg(target_os = "windows")]
 29    {
 30        path = path.replace("/", "\\");
 31        if path.starts_with("\\") {
 32            path = format!("C:{}", path);
 33        }
 34    }
 35
 36    TokenStream::from(quote! {
 37        #path
 38    })
 39}
 40
 41/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
 42/// But if the target OS is not Windows, the URI is returned as is.
 43///
 44/// # Example
 45/// ```rust
 46/// use util_macros::uri;
 47///
 48/// let uri = uri!("file:///path/to/file");
 49/// #[cfg(target_os = "windows")]
 50/// assert_eq!(uri, "file:///C:/path/to/file");
 51/// #[cfg(not(target_os = "windows"))]
 52/// assert_eq!(uri, "file:///path/to/file");
 53/// ```
 54#[proc_macro]
 55pub fn uri(input: TokenStream) -> TokenStream {
 56    let uri = parse_macro_input!(input as LitStr);
 57    let uri = uri.value();
 58
 59    #[cfg(target_os = "windows")]
 60    let uri = uri.replace("file:///", "file:///C:/");
 61
 62    TokenStream::from(quote! {
 63        #uri
 64    })
 65}
 66
 67/// This macro replaces the line endings `\n` with `\r\n` for Windows.
 68/// But if the target OS is not Windows, the line endings are returned as is.
 69///
 70/// # Example
 71/// ```rust
 72/// use util_macros::line_endings;
 73///
 74/// let text = line_endings!("Hello\nWorld");
 75/// #[cfg(target_os = "windows")]
 76/// assert_eq!(text, "Hello\r\nWorld");
 77/// #[cfg(not(target_os = "windows"))]
 78/// assert_eq!(text, "Hello\nWorld");
 79/// ```
 80#[proc_macro]
 81pub fn line_endings(input: TokenStream) -> TokenStream {
 82    let text = parse_macro_input!(input as LitStr);
 83    let text = text.value();
 84
 85    #[cfg(target_os = "windows")]
 86    let text = text.replace("\n", "\r\n");
 87
 88    TokenStream::from(quote! {
 89        #text
 90    })
 91}
 92
 93/// Inner data for the perf macro.
 94#[derive(Default)]
 95struct PerfArgs {
 96    /// How many times to loop a test before rerunning the test binary. If left
 97    /// empty, the test harness will auto-determine this value.
 98    iterations: Option<syn::Expr>,
 99    /// How much this test's results should be weighed when comparing across runs.
100    /// If unspecified, defaults to `WEIGHT_DEFAULT` (50).
101    weight: Option<syn::Expr>,
102    /// How relevant a benchmark is to overall performance. See docs on the enum
103    /// for details. If unspecified, `Average` is selected.
104    importance: Importance,
105}
106
107#[warn(clippy::all, clippy::pedantic)]
108impl PerfArgs {
109    /// Parses attribute arguments into a `PerfArgs`.
110    fn parse_into(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
111        if meta.path.is_ident("iterations") {
112            self.iterations = Some(meta.value()?.parse()?);
113        } else if meta.path.is_ident("weight") {
114            self.weight = Some(meta.value()?.parse()?);
115        } else if meta.path.is_ident("critical") {
116            self.importance = Importance::Critical;
117        } else if meta.path.is_ident("important") {
118            self.importance = Importance::Important;
119        } else if meta.path.is_ident("average") {
120            // This shouldn't be specified manually, but oh well.
121            self.importance = Importance::Average;
122        } else if meta.path.is_ident("iffy") {
123            self.importance = Importance::Iffy;
124        } else if meta.path.is_ident("fluff") {
125            self.importance = Importance::Fluff;
126        } else {
127            return Err(syn::Error::new_spanned(meta.path, "unexpected identifier"));
128        }
129        Ok(())
130    }
131}
132
133/// Marks a test as perf-sensitive, to be triaged when checking the performance
134/// of a build. This also automatically applies `#[test]`.
135///
136///
137/// # Usage
138/// Applying this attribute to a test marks it as average importance by default.
139/// There are 4 levels of importance (`Critical`, `Important`, `Average`, `Fluff`);
140/// see the documentation on `Importance` for details. Add the importance as a
141/// parameter to override the default (e.g. `#[perf(important)]`).
142///
143/// Each test also has a weight factor. This is irrelevant on its own, but is considered
144/// when comparing results across different runs. By default, this is set to 50;
145/// pass `weight = n` as a parameter to override this. Note that this value is only
146/// relevant within its importance category.
147///
148/// By default, the number of iterations when profiling this test is auto-determined.
149/// If this needs to be overwritten, pass the desired iteration count as a parameter
150/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test
151/// an arbitrary number times; this flag just sets the number of executions before the
152/// process is restarted and global state is reset.
153///
154/// This attribute should probably not be applied to tests that do any significant
155/// disk IO, as locks on files may not be released in time when repeating a test many
156/// times. This might lead to spurious failures.
157///
158/// # Examples
159/// ```rust
160/// use util_macros::perf;
161///
162/// #[perf]
163/// fn generic_test() {
164///     // Test goes here.
165/// }
166///
167/// #[perf(fluff, weight = 30)]
168/// fn cold_path_test() {
169///     // Test goes here.
170/// }
171/// ```
172///
173/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
174/// be used with automatic iterations.
175/// ```rust,ignore
176/// use util_macros::perf;
177///
178/// #[perf(iterations = 1, critical)]
179/// #[gpui::test]
180/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
181///     // Test goes here.
182/// }
183/// ```
184#[proc_macro_attribute]
185#[warn(clippy::all, clippy::pedantic)]
186pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
187    let mut args = PerfArgs::default();
188    let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta));
189    parse_macro_input!(our_attr with parser);
190
191    let ItemFn {
192        attrs: mut attrs_main,
193        vis,
194        sig: mut sig_main,
195        block,
196    } = parse_macro_input!(input as ItemFn);
197    attrs_main.push(parse_quote!(#[test]));
198    attrs_main.push(parse_quote!(#[allow(non_snake_case)]));
199
200    let fns = if cfg!(perf_enabled) {
201        #[allow(clippy::wildcard_imports, reason = "We control the other side")]
202        use consts::*;
203
204        // Make the ident obvious when calling, for the test parser.
205        // Also set up values for the second metadata-returning "test".
206        let mut new_ident_main = sig_main.ident.to_string();
207        let mut new_ident_meta = new_ident_main.clone();
208        new_ident_main.push_str(SUF_NORMAL);
209        new_ident_meta.push_str(SUF_MDATA);
210
211        let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span());
212        sig_main.ident = new_ident_main;
213
214        // We don't want any nonsense if the original test had a weird signature.
215        let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span());
216        let sig_meta = parse_quote!(fn #new_ident_meta());
217        let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]);
218
219        // Make the test loop as the harness instructs it to.
220        let block_main = {
221            // The perf harness will pass us the value in an env var. Even if we
222            // have a preset value, just do this to keep the code paths unified.
223            parse_quote!({
224                let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
225                for _ in 0..iter_count {
226                    #block
227                }
228            })
229        };
230        let importance = format!("{}", args.importance);
231        let block_meta = {
232            // This function's job is to just print some relevant info to stdout,
233            // based on the params this attr is passed. It's not an actual test.
234            // Since we use a custom attr set on our metadata fn, it shouldn't
235            // cause problems with xfail tests.
236            let q_iter = if let Some(iter) = args.iterations {
237                quote! {
238                    println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter);
239                }
240            } else {
241                quote! {}
242            };
243            let weight = args
244                .weight
245                .unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT });
246            parse_quote!({
247                #q_iter
248                println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight);
249                println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance);
250                println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER);
251            })
252        };
253
254        vec![
255            // The real test.
256            ItemFn {
257                attrs: attrs_main,
258                vis: vis.clone(),
259                sig: sig_main,
260                block: block_main,
261            },
262            // The fake test.
263            ItemFn {
264                attrs: attrs_meta,
265                vis,
266                sig: sig_meta,
267                block: block_meta,
268            },
269        ]
270    } else {
271        vec![ItemFn {
272            attrs: attrs_main,
273            vis,
274            sig: sig_main,
275            block,
276        }]
277    };
278
279    fns.into_iter()
280        .flat_map(|f| TokenStream::from(f.into_token_stream()))
281        .collect()
282}