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
8/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
9/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
10/// returned unmodified.
11///
12/// # Example
13/// ```rust
14/// use util_macros::path;
15///
16/// let path = path!("/Users/user/file.txt");
17/// #[cfg(target_os = "windows")]
18/// assert_eq!(path, "C:\\Users\\user\\file.txt");
19/// #[cfg(not(target_os = "windows"))]
20/// assert_eq!(path, "/Users/user/file.txt");
21/// ```
22#[proc_macro]
23pub fn path(input: TokenStream) -> TokenStream {
24 let path = parse_macro_input!(input as LitStr);
25 let mut path = path.value();
26
27 #[cfg(target_os = "windows")]
28 {
29 path = path.replace("/", "\\");
30 if path.starts_with("\\") {
31 path = format!("C:{}", path);
32 }
33 }
34
35 TokenStream::from(quote! {
36 #path
37 })
38}
39
40/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
41/// But if the target OS is not Windows, the URI is returned as is.
42///
43/// # Example
44/// ```rust
45/// use util_macros::uri;
46///
47/// let uri = uri!("file:///path/to/file");
48/// #[cfg(target_os = "windows")]
49/// assert_eq!(uri, "file:///C:/path/to/file");
50/// #[cfg(not(target_os = "windows"))]
51/// assert_eq!(uri, "file:///path/to/file");
52/// ```
53#[proc_macro]
54pub fn uri(input: TokenStream) -> TokenStream {
55 let uri = parse_macro_input!(input as LitStr);
56 let uri = uri.value();
57
58 #[cfg(target_os = "windows")]
59 let uri = uri.replace("file:///", "file:///C:/");
60
61 TokenStream::from(quote! {
62 #uri
63 })
64}
65
66/// This macro replaces the line endings `\n` with `\r\n` for Windows.
67/// But if the target OS is not Windows, the line endings are returned as is.
68///
69/// # Example
70/// ```rust
71/// use util_macros::line_endings;
72///
73/// let text = line_endings!("Hello\nWorld");
74/// #[cfg(target_os = "windows")]
75/// assert_eq!(text, "Hello\r\nWorld");
76/// #[cfg(not(target_os = "windows"))]
77/// assert_eq!(text, "Hello\nWorld");
78/// ```
79#[proc_macro]
80pub fn line_endings(input: TokenStream) -> TokenStream {
81 let text = parse_macro_input!(input as LitStr);
82 let text = text.value();
83
84 #[cfg(target_os = "windows")]
85 let text = text.replace("\n", "\r\n");
86
87 TokenStream::from(quote! {
88 #text
89 })
90}
91
92/// Inner data for the perf macro.
93struct PerfArgs {
94 /// How many times to loop a test before rerunning the test binary.
95 /// If left empty, the test harness will auto-determine this value.
96 iterations: Option<syn::Expr>,
97}
98
99impl syn::parse::Parse for PerfArgs {
100 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
101 if input.is_empty() {
102 return Ok(PerfArgs { iterations: None });
103 }
104
105 let mut iterations = None;
106 // In principle we only have one possible argument, but leave this as
107 // a loop in case we expand this in the future.
108 for meta in
109 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?
110 {
111 match &meta {
112 syn::Meta::NameValue(meta_name_value) => {
113 if meta_name_value.path.is_ident("iterations") {
114 iterations = Some(meta_name_value.value.clone());
115 } else {
116 return Err(syn::Error::new_spanned(
117 &meta_name_value.path,
118 "unexpected argument, expected 'iterations'",
119 ));
120 }
121 }
122 _ => {
123 return Err(syn::Error::new_spanned(
124 meta,
125 "expected name-value argument like 'iterations = 1'",
126 ));
127 }
128 }
129 }
130
131 Ok(PerfArgs { iterations })
132 }
133}
134
135/// Marks a test as perf-sensitive, to be triaged when checking the performance
136/// of a build. This also automatically applies `#[test]`.
137///
138/// By default, the number of iterations when profiling this test is auto-determined.
139/// If this needs to be overwritten, pass the desired iteration count to the macro
140/// as a parameter (`#[perf(iterations = n)]`). Note that the actual profiler may still
141/// run the test an arbitrary number times; this flag just sets the number of executions
142/// before the process is restarted and global state is reset.
143///
144/// # Usage notes
145/// This should probably not be applied to tests that do any significant fs IO, as
146/// locks on files may not be released in time when repeating a test many times. This
147/// might lead to spurious failures.
148///
149/// # Examples
150/// ```rust
151/// use util_macros::perf;
152///
153/// #[perf]
154/// fn expensive_computation_test() {
155/// // Test goes here.
156/// }
157/// ```
158///
159/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
160/// be used with automatic iterations.
161/// ```rust,ignore
162/// use util_macros::perf;
163///
164/// #[perf(iterations = 1)]
165/// #[gpui::test]
166/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
167/// // Test goes here.
168/// }
169/// ```
170#[proc_macro_attribute]
171pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
172 // If any of the below constants are changed, make sure to also update the perf
173 // profiler to match!
174
175 /// The suffix on tests marked with `#[perf]`.
176 const SUF_NORMAL: &str = "__ZED_PERF";
177 /// The suffix on tests marked with `#[perf(iterations = n)]`.
178 const SUF_FIXED: &str = "__ZED_PERF_FIXEDITER";
179 /// The env var in which we pass the iteration count to our tests.
180 const ITER_ENV_VAR: &str = "ZED_PERF_ITER";
181
182 let iter_count = parse_macro_input!(our_attr as PerfArgs).iterations;
183
184 let ItemFn {
185 mut attrs,
186 vis,
187 mut sig,
188 block,
189 } = parse_macro_input!(input as ItemFn);
190 attrs.push(parse_quote!(#[test]));
191 attrs.push(parse_quote!(#[allow(non_snake_case)]));
192
193 let block: Box<syn::Block> = if cfg!(perf_enabled) {
194 // Make the ident obvious when calling, for the test parser.
195 let mut new_ident = sig.ident.to_string();
196 if iter_count.is_some() {
197 new_ident.push_str(SUF_FIXED);
198 } else {
199 new_ident.push_str(SUF_NORMAL);
200 }
201
202 let new_ident = syn::Ident::new(&new_ident, sig.ident.span());
203 sig.ident = new_ident;
204 // If we have a preset iteration count, just use that.
205 if let Some(iter_count) = iter_count {
206 parse_quote!({
207 for _ in 0..#iter_count {
208 #block
209 }
210 })
211 } else {
212 // Otherwise, the perf harness will pass us the value in an env var.
213 parse_quote!({
214 let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
215 for _ in 0..iter_count {
216 #block
217 }
218 })
219 }
220 } else {
221 block
222 };
223
224 ItemFn {
225 attrs,
226 vis,
227 sig,
228 block,
229 }
230 .into_token_stream()
231 .into()
232}