Skip to main content

test_fork_macros/
lib.rs

1// Copyright (C) 2025-2026 Daniel Mueller <deso@posteo.net>
2// SPDX-License-Identifier: (Apache-2.0 OR MIT)
3
4//! The procedural macro powering `test-fork`.
5
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8
9use std::ops::Deref as _;
10
11use proc_macro::TokenStream;
12use proc_macro2::Ident;
13use proc_macro2::Span;
14use proc_macro2::TokenStream as Tokens;
15
16use quote::quote;
17use quote::ToTokens as _;
18
19use syn::parse_macro_input;
20use syn::Attribute;
21use syn::Error;
22use syn::FnArg;
23use syn::ItemFn;
24use syn::Pat;
25use syn::Result;
26use syn::ReturnType;
27use syn::Signature;
28use syn::Type;
29
30
31#[derive(Debug)]
32enum Kind {
33    Test,
34    Bench,
35}
36
37impl Kind {
38    #[inline]
39    fn as_str(&self) -> &str {
40        match self {
41            Self::Test => "test",
42            Self::Bench => "bench",
43        }
44    }
45}
46
47
48/// A procedural macro for running a test in a separate process.
49///
50/// # Example
51///
52/// Use the attribute for all tests in scope:
53/// ```rust,ignore
54/// use test_fork::test;
55///
56/// #[test]
57/// fn test1() {
58///   assert_eq!(2 + 2, 4);
59/// }
60/// ```
61///
62/// Use it only on a single test:
63/// ```rust,ignore
64/// #[test_fork::test]
65/// fn test2() {
66///   assert_eq!(2 + 3, 5);
67/// }
68/// ```
69#[proc_macro_attribute]
70pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
71    let input_fn = parse_macro_input!(item as ItemFn);
72
73    let has_test = input_fn
74        .attrs
75        .iter()
76        .any(|attr| is_attribute_kind(Kind::Test, attr));
77    let inner_test = if has_test {
78        quote! {}
79    } else {
80        quote! { #[::core::prelude::v1::test] }
81    };
82
83    try_test(attr, input_fn, inner_test)
84        .unwrap_or_else(syn::Error::into_compile_error)
85        .into()
86}
87
88
89/// A procedural macro for running a benchmark in a separate process.
90///
91/// # Example
92///
93/// Use the attribute for all benchmarks in scope:
94/// ```rust,ignore
95/// use test_fork::bench;
96///
97/// #[bench]
98/// fn bench1(b: &mut Bencher) {
99///   b.iter(|| sleep(Duration::from_millis(1)));
100/// }
101/// ```
102///
103/// Use it only on a single benchmark:
104/// ```rust,ignore
105/// #[test_fork::bench]
106/// fn bench2(b: &mut Bencher) {
107///   b.iter(|| sleep(Duration::from_millis(1)));
108/// }
109#[cfg(all(feature = "unstable", feature = "unsound"))]
110#[cfg_attr(docsrs, doc(cfg(all(feature = "unstable", feature = "unsound"))))]
111#[proc_macro_attribute]
112pub fn bench(attr: TokenStream, item: TokenStream) -> TokenStream {
113    let input_fn = parse_macro_input!(item as ItemFn);
114
115    let has_bench = input_fn
116        .attrs
117        .iter()
118        .any(|attr| is_attribute_kind(Kind::Bench, attr));
119    let inner_bench = if has_bench {
120        quote! {}
121    } else {
122        quote! { #[::core::prelude::v1::bench] }
123    };
124
125    try_bench(attr, input_fn, inner_bench)
126        .unwrap_or_else(syn::Error::into_compile_error)
127        .into()
128}
129
130
131/// A procedural macro for running a test or benchmark in a separate
132/// process.
133///
134/// This attribute is able to cater to both tests and benchmarks, while
135/// #[[macro@test]] is specific to tests and #[[macro@bench]] to
136/// benchmarks.
137///
138/// Contrary to both, this attribute does not in itself make a function
139/// a test/benchmark, so it will *always* have to be combined with an
140/// additional "inner" attribute. However, it can be more convenient for
141/// annotating only a sub-set of tests/benchmarks for running in
142/// separate processes, especially when non-standard attributes are
143/// involved:
144///
145/// # Example
146///
147/// ```rust,ignore
148/// use test_fork::fork;
149///
150/// #[fork]
151/// #[test]
152/// fn test3() {
153///   assert_eq!(2 + 4, 6);
154/// }
155///
156/// #[fork]
157/// #[bench]
158/// fn bench3(b: &mut Bencher) {
159///   b.iter(|| sleep(Duration::from_millis(1)));
160/// }
161/// ```
162#[proc_macro_attribute]
163pub fn fork(attr: TokenStream, item: TokenStream) -> TokenStream {
164    let supports_bench = cfg!(all(feature = "unstable", feature = "unsound"));
165    let input_fn = parse_macro_input!(item as ItemFn);
166
167    let has_test = input_fn
168        .attrs
169        .iter()
170        .any(|attr| is_attribute_kind(Kind::Test, attr));
171    let has_bench = supports_bench
172        && input_fn
173            .attrs
174            .iter()
175            .any(|attr| is_attribute_kind(Kind::Bench, attr));
176
177    let inner_attr = quote! {};
178    if has_test {
179        try_test(attr, input_fn, inner_attr)
180    } else if has_bench {
181        try_bench(attr, input_fn, inner_attr)
182    } else {
183        let inner_attr = if parse_bench_sig(&input_fn.sig).is_some() {
184            "#[bench]"
185        } else {
186            "#[test]"
187        };
188
189        Err(Error::new_spanned(
190            Tokens::from(attr),
191            format!("test_fork::fork requires an inner {inner_attr} attribute"),
192        ))
193    }
194    .unwrap_or_else(syn::Error::into_compile_error)
195    .into()
196}
197
198
199/// Check whether given attribute is a test or bench attribute of the
200/// form:
201/// - `#[<kind>]`
202/// - `#[core::prelude::*::<kind>]` or `#[::core::prelude::*::<kind>]`
203/// - `#[std::prelude::*::<kind>]` or `#[::std::prelude::*::<kind>]`
204fn is_attribute_kind(kind: Kind, attr: &Attribute) -> bool {
205    let path = match &attr.meta {
206        syn::Meta::Path(path) => path,
207        _ => return false,
208    };
209    let candidates = [
210        ["core", "prelude", "*", kind.as_str()],
211        ["std", "prelude", "*", kind.as_str()],
212    ];
213
214    #[expect(clippy::indexing_slicing)]
215    if path.leading_colon.is_none()
216        && path.segments.len() == 1
217        && path.segments[0].arguments.is_none()
218        && path.segments[0].ident == kind.as_str()
219    {
220        return true;
221    } else if path.segments.len() != candidates[0].len() {
222        return false;
223    }
224    candidates.into_iter().any(|segments| {
225        path.segments.iter().zip(segments).all(|(segment, path)| {
226            segment.arguments.is_none() && (path == "*" || segment.ident == path)
227        })
228    })
229}
230
231fn try_test(attr: TokenStream, input_fn: ItemFn, inner_test: Tokens) -> Result<Tokens> {
232    if !attr.is_empty() {
233        return Err(Error::new_spanned(
234            Tokens::from(attr),
235            "the attribute does not currently accept arguments",
236        ))
237    }
238
239    let ItemFn {
240        attrs,
241        vis,
242        mut sig,
243        block,
244    } = input_fn;
245
246    let test_name = sig.ident.clone();
247    let mut body_fn_sig = sig.clone();
248    body_fn_sig.ident = Ident::new("body_fn", Span::call_site());
249    // Our tests currently basically have to return (), because we don't
250    // have a good way of conveying the result back from the child
251    // process.
252    sig.output = ReturnType::Default;
253
254    let augmented_test = quote! {
255        #inner_test
256        #(#attrs)*
257        #vis #sig {
258            #body_fn_sig
259            #block
260
261            ::test_fork::test_fork_core::fork(
262                ::test_fork::test_fork_core::fork_id!(),
263                ::test_fork::test_fork_core::fork_test_name!(#test_name),
264                body_fn as fn() -> _,
265            ).expect("forking test failed")
266        }
267    };
268
269    Ok(augmented_test)
270}
271
272fn parse_bench_sig(sig: &Signature) -> Option<(Pat, Type)> {
273    if sig.inputs.len() != 1 {
274        return None
275    }
276
277    if let FnArg::Typed(pat_type) = sig.inputs.first()? {
278        let ty = match pat_type.ty.deref() {
279            Type::Reference(ty_ref) => ty_ref.elem.clone(),
280            _ => return None,
281        };
282        Some((*pat_type.pat.clone(), *ty))
283    } else {
284        None
285    }
286}
287
288fn try_bench(attr: TokenStream, input_fn: ItemFn, inner_bench: Tokens) -> Result<Tokens> {
289    if !attr.is_empty() {
290        return Err(Error::new_spanned(
291            Tokens::from(attr),
292            "the attribute does not currently accept arguments",
293        ))
294    }
295
296    let ItemFn {
297        attrs,
298        vis,
299        mut sig,
300        block,
301    } = input_fn;
302
303    let (bencher_name, bencher_ty) = parse_bench_sig(&sig).ok_or_else(|| {
304        Error::new_spanned(
305            sig.to_token_stream(),
306            "benchmark function has unexpected signature (expected single `&mut Bencher` argument)",
307        )
308    })?;
309
310    let test_name = sig.ident.clone();
311    let mut body_fn_sig = sig.clone();
312    body_fn_sig.ident = Ident::new("body_fn", Span::call_site());
313    sig.output = ReturnType::Default;
314
315    let augmented_bench = quote! {
316        #inner_bench
317        #(#attrs)*
318        #vis #sig {
319            #body_fn_sig
320            #block
321
322            use ::std::mem::size_of;
323            use ::std::mem::transmute;
324
325            type BencherBuf = [u8; size_of::<#bencher_ty>()];
326
327            // SAFETY: Probably unsound. We can't guarantee that the
328            //         `Bencher` type is just a bunch of bytes that we
329            //         can copy around. And yet, that's the best we can
330            //         do.
331            let buf_ref = unsafe {
332                transmute::<&mut #bencher_ty, &mut BencherBuf>(#bencher_name)
333            };
334
335            fn wrapper_fn(buf_ref: &mut [u8]) {
336                let buf_ref = <&mut BencherBuf>::try_from(buf_ref).unwrap();
337                // SAFETY: See above.
338                let bench_ref = unsafe {
339                    transmute::<&mut BencherBuf, &mut #bencher_ty>(buf_ref)
340                };
341                let () = body_fn(bench_ref);
342            }
343
344            ::test_fork::test_fork_core::fork_in_out(
345                ::test_fork::test_fork_core::fork_id!(),
346                ::test_fork::test_fork_core::fork_test_name!(#test_name),
347                wrapper_fn as fn(&mut [u8]) -> _,
348                buf_ref,
349            ).expect("forking test failed")
350        }
351    };
352
353    Ok(augmented_bench)
354}