1#![allow(clippy::test_attr_in_doctest)]
2
3use convert_case::{Case, Casing};
4use perf::*;
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::{ToTokens, format_ident, quote};
8use syn::{
9 Data, DeriveInput, Expr, ExprArray, ExprLit, Fields, ItemFn, Lit, LitStr, MetaNameValue, Token,
10 parse_macro_input, parse_quote, punctuated::Punctuated,
11};
12
13/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
14/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
15/// returned unmodified.
16///
17/// # Example
18/// ```rust
19/// use util_macros::path;
20///
21/// let path = path!("/Users/user/file.txt");
22/// #[cfg(target_os = "windows")]
23/// assert_eq!(path, "C:\\Users\\user\\file.txt");
24/// #[cfg(not(target_os = "windows"))]
25/// assert_eq!(path, "/Users/user/file.txt");
26/// ```
27#[proc_macro]
28pub fn path(input: TokenStream) -> TokenStream {
29 let path = parse_macro_input!(input as LitStr);
30
31 #[cfg(target_os = "windows")]
32 {
33 let mut path = path.value();
34 path = path.replace("/", "\\");
35 if path.starts_with("\\") {
36 path = format!("C:{}", path);
37 }
38 return TokenStream::from(quote! {
39 #path
40 });
41 }
42
43 #[cfg(not(target_os = "windows"))]
44 {
45 let path = path.value();
46 return TokenStream::from(quote! {
47 #path
48 });
49 }
50}
51
52/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
53/// But if the target OS is not Windows, the URI is returned as is.
54///
55/// # Example
56/// ```rust
57/// use util_macros::uri;
58///
59/// let uri = uri!("file:///path/to/file");
60/// #[cfg(target_os = "windows")]
61/// assert_eq!(uri, "file:///C:/path/to/file");
62/// #[cfg(not(target_os = "windows"))]
63/// assert_eq!(uri, "file:///path/to/file");
64/// ```
65#[proc_macro]
66pub fn uri(input: TokenStream) -> TokenStream {
67 let uri = parse_macro_input!(input as LitStr);
68 let uri = uri.value();
69
70 #[cfg(target_os = "windows")]
71 let uri = uri.replace("file:///", "file:///C:/");
72
73 TokenStream::from(quote! {
74 #uri
75 })
76}
77
78/// This macro replaces the line endings `\n` with `\r\n` for Windows.
79/// But if the target OS is not Windows, the line endings are returned as is.
80///
81/// # Example
82/// ```rust
83/// use util_macros::line_endings;
84///
85/// let text = line_endings!("Hello\nWorld");
86/// #[cfg(target_os = "windows")]
87/// assert_eq!(text, "Hello\r\nWorld");
88/// #[cfg(not(target_os = "windows"))]
89/// assert_eq!(text, "Hello\nWorld");
90/// ```
91#[proc_macro]
92pub fn line_endings(input: TokenStream) -> TokenStream {
93 let text = parse_macro_input!(input as LitStr);
94 let text = text.value();
95
96 #[cfg(target_os = "windows")]
97 let text = text.replace("\n", "\r\n");
98
99 TokenStream::from(quote! {
100 #text
101 })
102}
103
104/// Inner data for the perf macro.
105#[derive(Default)]
106struct PerfArgs {
107 /// How many times to loop a test before rerunning the test binary. If left
108 /// empty, the test harness will auto-determine this value.
109 iterations: Option<syn::Expr>,
110 /// How much this test's results should be weighed when comparing across runs.
111 /// If unspecified, defaults to `WEIGHT_DEFAULT` (50).
112 weight: Option<syn::Expr>,
113 /// How relevant a benchmark is to overall performance. See docs on the enum
114 /// for details. If unspecified, `Average` is selected.
115 importance: Importance,
116}
117
118#[warn(clippy::all, clippy::pedantic)]
119impl PerfArgs {
120 /// Parses attribute arguments into a `PerfArgs`.
121 fn parse_into(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
122 if meta.path.is_ident("iterations") {
123 self.iterations = Some(meta.value()?.parse()?);
124 } else if meta.path.is_ident("weight") {
125 self.weight = Some(meta.value()?.parse()?);
126 } else if meta.path.is_ident("critical") {
127 self.importance = Importance::Critical;
128 } else if meta.path.is_ident("important") {
129 self.importance = Importance::Important;
130 } else if meta.path.is_ident("average") {
131 // This shouldn't be specified manually, but oh well.
132 self.importance = Importance::Average;
133 } else if meta.path.is_ident("iffy") {
134 self.importance = Importance::Iffy;
135 } else if meta.path.is_ident("fluff") {
136 self.importance = Importance::Fluff;
137 } else {
138 return Err(syn::Error::new_spanned(meta.path, "unexpected identifier"));
139 }
140 Ok(())
141 }
142}
143
144/// Marks a test as perf-sensitive, to be triaged when checking the performance
145/// of a build. This also automatically applies `#[test]`.
146///
147///
148/// # Usage
149/// Applying this attribute to a test marks it as average importance by default.
150/// There are 4 levels of importance (`Critical`, `Important`, `Average`, `Fluff`);
151/// see the documentation on `Importance` for details. Add the importance as a
152/// parameter to override the default (e.g. `#[perf(important)]`).
153///
154/// Each test also has a weight factor. This is irrelevant on its own, but is considered
155/// when comparing results across different runs. By default, this is set to 50;
156/// pass `weight = n` as a parameter to override this. Note that this value is only
157/// relevant within its importance category.
158///
159/// By default, the number of iterations when profiling this test is auto-determined.
160/// If this needs to be overwritten, pass the desired iteration count as a parameter
161/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test
162/// an arbitrary number times; this flag just sets the number of executions before the
163/// process is restarted and global state is reset.
164///
165/// This attribute should probably not be applied to tests that do any significant
166/// disk IO, as locks on files may not be released in time when repeating a test many
167/// times. This might lead to spurious failures.
168///
169/// # Examples
170/// ```rust
171/// use util_macros::perf;
172///
173/// #[perf]
174/// fn generic_test() {
175/// // Test goes here.
176/// }
177///
178/// #[perf(fluff, weight = 30)]
179/// fn cold_path_test() {
180/// // Test goes here.
181/// }
182/// ```
183///
184/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
185/// be used with automatic iterations.
186/// ```rust,ignore
187/// use util_macros::perf;
188///
189/// #[perf(iterations = 1, critical)]
190/// #[gpui::test]
191/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
192/// // Test goes here.
193/// }
194/// ```
195#[proc_macro_attribute]
196#[warn(clippy::all, clippy::pedantic)]
197pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
198 let mut args = PerfArgs::default();
199 let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta));
200 parse_macro_input!(our_attr with parser);
201
202 let ItemFn {
203 attrs: mut attrs_main,
204 vis,
205 sig: mut sig_main,
206 block,
207 } = parse_macro_input!(input as ItemFn);
208 attrs_main.push(parse_quote!(#[test]));
209 attrs_main.push(parse_quote!(#[allow(non_snake_case)]));
210
211 let fns = if cfg!(perf_enabled) {
212 #[allow(clippy::wildcard_imports, reason = "We control the other side")]
213 use consts::*;
214
215 // Make the ident obvious when calling, for the test parser.
216 // Also set up values for the second metadata-returning "test".
217 let mut new_ident_main = sig_main.ident.to_string();
218 let mut new_ident_meta = new_ident_main.clone();
219 new_ident_main.push_str(SUF_NORMAL);
220 new_ident_meta.push_str(SUF_MDATA);
221
222 let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span());
223 sig_main.ident = new_ident_main;
224
225 // We don't want any nonsense if the original test had a weird signature.
226 let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span());
227 let sig_meta = parse_quote!(fn #new_ident_meta());
228 let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]);
229
230 // Make the test loop as the harness instructs it to.
231 let block_main = {
232 // The perf harness will pass us the value in an env var. Even if we
233 // have a preset value, just do this to keep the code paths unified.
234 parse_quote!({
235 let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
236 for _ in 0..iter_count {
237 #block
238 }
239 })
240 };
241 let importance = format!("{}", args.importance);
242 let block_meta = {
243 // This function's job is to just print some relevant info to stdout,
244 // based on the params this attr is passed. It's not an actual test.
245 // Since we use a custom attr set on our metadata fn, it shouldn't
246 // cause problems with xfail tests.
247 let q_iter = if let Some(iter) = args.iterations {
248 quote! {
249 println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter);
250 }
251 } else {
252 quote! {}
253 };
254 let weight = args
255 .weight
256 .unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT });
257 parse_quote!({
258 #q_iter
259 println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight);
260 println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance);
261 println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER);
262 })
263 };
264
265 vec![
266 // The real test.
267 ItemFn {
268 attrs: attrs_main,
269 vis: vis.clone(),
270 sig: sig_main,
271 block: block_main,
272 },
273 // The fake test.
274 ItemFn {
275 attrs: attrs_meta,
276 vis,
277 sig: sig_meta,
278 block: block_meta,
279 },
280 ]
281 } else {
282 vec![ItemFn {
283 attrs: attrs_main,
284 vis,
285 sig: sig_main,
286 block,
287 }]
288 };
289
290 fns.into_iter()
291 .flat_map(|f| TokenStream::from(f.into_token_stream()))
292 .collect()
293}
294
295#[proc_macro_derive(FieldAccessByEnum, attributes(field_access_by_enum))]
296pub fn derive_field_access_by_enum(input: TokenStream) -> TokenStream {
297 let input = parse_macro_input!(input as DeriveInput);
298
299 let struct_name = &input.ident;
300
301 let mut enum_name = None;
302 let mut enum_attrs: Vec<TokenStream2> = Vec::new();
303
304 for attr in &input.attrs {
305 if attr.path().is_ident("field_access_by_enum") {
306 let name_values: Punctuated<MetaNameValue, Token![,]> =
307 attr.parse_args_with(Punctuated::parse_terminated).unwrap();
308 for name_value in name_values {
309 if name_value.path.is_ident("enum_name") {
310 let value = name_value.value;
311 match value {
312 Expr::Lit(ExprLit {
313 lit: Lit::Str(name),
314 ..
315 }) => enum_name = Some(name.value()),
316 _ => panic!("Expected string literal in enum_name attribute"),
317 }
318 } else if name_value.path.is_ident("enum_attrs") {
319 let value = name_value.value;
320 match value {
321 Expr::Array(ExprArray { elems, .. }) => {
322 for elem in elems {
323 enum_attrs.push(quote!(#[#elem]));
324 }
325 }
326 _ => panic!("Expected array literal in enum_attr attribute"),
327 }
328 } else {
329 if let Some(ident) = name_value.path.get_ident() {
330 panic!("Unrecognized argument name {}", ident);
331 } else {
332 panic!("Unrecognized argument {:?}", name_value.path);
333 }
334 }
335 }
336 }
337 }
338 let Some(enum_name) = enum_name else {
339 panic!("#[field_access_by_enum(enum_name = \"...\")] attribute is required");
340 };
341 let enum_ident = format_ident!("{}", enum_name);
342
343 let fields = match input.data {
344 Data::Struct(data_struct) => match data_struct.fields {
345 Fields::Named(fields) => fields.named,
346 _ => panic!("FieldAccessByEnum can only be derived for structs with named fields"),
347 },
348 _ => panic!("FieldAccessByEnum can only be derived for structs"),
349 };
350
351 if fields.is_empty() {
352 panic!("FieldAccessByEnum cannot be derived for structs with no fields");
353 }
354
355 let mut enum_variants = Vec::new();
356 let mut get_match_arms = Vec::new();
357 let mut set_match_arms = Vec::new();
358 let mut field_types = Vec::new();
359
360 for field in fields.iter() {
361 let field_name = field.ident.as_ref().unwrap();
362 let variant_name = field_name.to_string().to_case(Case::Pascal);
363 let variant_ident = format_ident!("{}", variant_name);
364 let field_type = &field.ty;
365
366 enum_variants.push(variant_ident.clone());
367 field_types.push(field_type);
368
369 get_match_arms.push(quote! {
370 #enum_ident::#variant_ident => &self.#field_name,
371 });
372
373 set_match_arms.push(quote! {
374 #enum_ident::#variant_ident => self.#field_name = value,
375 });
376 }
377
378 let first_type = &field_types[0];
379 let all_same_type = field_types
380 .iter()
381 .all(|ty| quote!(#ty).to_string() == quote!(#first_type).to_string());
382 if !all_same_type {
383 panic!("Fields have different types.");
384 }
385 let field_value_type = quote! { #first_type };
386
387 let expanded = quote! {
388 #(#enum_attrs)*
389 pub enum #enum_ident {
390 #(#enum_variants),*
391 }
392
393 impl util::FieldAccessByEnum<#field_value_type> for #struct_name {
394 type Field = #enum_ident;
395
396 fn get_field_by_enum(&self, field: Self::Field) -> &#field_value_type {
397 match field {
398 #(#get_match_arms)*
399 }
400 }
401
402 fn set_field_by_enum(&mut self, field: Self::Field, value: #field_value_type) {
403 match field {
404 #(#set_match_arms)*
405 }
406 }
407 }
408 };
409
410 TokenStream::from(expanded)
411}