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/// # Usage
137/// Applying this attribute to a test marks it as average importance by default.
138/// There are 5 levels of importance (`Critical`, `Important`, `Average`, `Iffy`,
139/// `Fluff`); see the documentation on `Importance` for details. Add the importance
140/// as a parameter to override the default (e.g. `#[perf(important)]`).
141///
142/// Each test also has a weight factor. This is irrelevant on its own, but is considered
143/// when comparing results across different runs. By default, this is set to 50;
144/// pass `weight = n` as a parameter to override this. Note that this value is only
145/// relevant within its importance category.
146///
147/// By default, the number of iterations when profiling this test is auto-determined.
148/// If this needs to be overwritten, pass the desired iteration count as a parameter
149/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test
150/// an arbitrary number times; this flag just sets the number of executions before the
151/// process is restarted and global state is reset.
152///
153/// This attribute should probably not be applied to tests that do any significant
154/// disk IO, as locks on files may not be released in time when repeating a test many
155/// times. This might lead to spurious failures.
156///
157/// # Examples
158/// ```rust
159/// use util_macros::perf;
160///
161/// #[perf]
162/// fn generic_test() {
163///     // Test goes here.
164/// }
165///
166/// #[perf(fluff, weight = 30)]
167/// fn cold_path_test() {
168///     // Test goes here.
169/// }
170/// ```
171///
172/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
173/// be used with automatic iterations.
174/// ```rust,ignore
175/// use util_macros::perf;
176///
177/// #[perf(iterations = 1, critical)]
178/// #[gpui::test]
179/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
180///     // Test goes here.
181/// }
182/// ```
183#[proc_macro_attribute]
184#[warn(clippy::all, clippy::pedantic)]
185pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
186    let mut args = PerfArgs::default();
187    let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta));
188    parse_macro_input!(our_attr with parser);
189
190    let ItemFn {
191        attrs: mut attrs_main,
192        vis,
193        sig: mut sig_main,
194        block,
195    } = parse_macro_input!(input as ItemFn);
196    if !attrs_main
197        .iter()
198        .any(|a| Some(&parse_quote!(test)) == a.path().segments.last())
199    {
200        attrs_main.push(parse_quote!(#[test]));
201    }
202    attrs_main.push(parse_quote!(#[allow(non_snake_case)]));
203
204    let fns = if cfg!(perf_enabled) {
205        #[allow(clippy::wildcard_imports, reason = "We control the other side")]
206        use consts::*;
207
208        // Make the ident obvious when calling, for the test parser.
209        // Also set up values for the second metadata-returning "test".
210        let mut new_ident_main = sig_main.ident.to_string();
211        let mut new_ident_meta = new_ident_main.clone();
212        new_ident_main.push_str(SUF_NORMAL);
213        new_ident_meta.push_str(SUF_MDATA);
214
215        let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span());
216        sig_main.ident = new_ident_main;
217
218        // We don't want any nonsense if the original test had a weird signature.
219        let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span());
220        let sig_meta = parse_quote!(fn #new_ident_meta());
221        let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]);
222
223        // Make the test loop as the harness instructs it to.
224        let block_main = {
225            // The perf harness will pass us the value in an env var. Even if we
226            // have a preset value, just do this to keep the code paths unified.
227            parse_quote!({
228                let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
229                for _ in 0..iter_count {
230                    #block
231                }
232            })
233        };
234        let importance = format!("{}", args.importance);
235        let block_meta = {
236            // This function's job is to just print some relevant info to stdout,
237            // based on the params this attr is passed. It's not an actual test.
238            // Since we use a custom attr set on our metadata fn, it shouldn't
239            // cause problems with xfail tests.
240            let q_iter = if let Some(iter) = args.iterations {
241                quote! {
242                    println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter);
243                }
244            } else {
245                quote! {}
246            };
247            let weight = args
248                .weight
249                .unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT });
250            parse_quote!({
251                #q_iter
252                println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight);
253                println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance);
254                println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER);
255            })
256        };
257
258        vec![
259            // The real test.
260            ItemFn {
261                attrs: attrs_main,
262                vis: vis.clone(),
263                sig: sig_main,
264                block: block_main,
265            },
266            // The fake test.
267            ItemFn {
268                attrs: attrs_meta,
269                vis,
270                sig: sig_meta,
271                block: block_meta,
272            },
273        ]
274    } else {
275        vec![ItemFn {
276            attrs: attrs_main,
277            vis,
278            sig: sig_main,
279            block,
280        }]
281    };
282
283    fns.into_iter()
284        .flat_map(|f| TokenStream::from(f.into_token_stream()))
285        .collect()
286}