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}