Skip to main content

open_gpui_util_macros/
util_macros.rs

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