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
  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}