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}