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}