@@ -22,9 +22,11 @@ extern crate alloc;
2222
2323use alloc:: string:: ToString ;
2424use proc_macro:: { Delimiter , Group , TokenStream , TokenTree } ;
25+ use proc_macro2:: TokenStream as TokenStream2 ;
2526use quote:: quote;
2627use syn:: spanned:: Spanned ;
2728use syn:: { parse, ImplItemFn , Token } ;
29+ use syn:: { parse_macro_input, Item } ;
2830
2931fn add_async_method ( mut parsed : ImplItemFn ) -> TokenStream {
3032 let output = quote ! {
@@ -294,3 +296,136 @@ pub fn drop_legacy_field_definition(expr: TokenStream) -> TokenStream {
294296 let out = syn:: Expr :: Struct ( st) ;
295297 quote ! { #out } . into ( )
296298}
299+
300+ /// An exposed test. This is a test that will run locally and also be
301+ /// made available to other crates that want to run it in their own context.
302+ ///
303+ /// For example:
304+ /// ```rust
305+ /// use lightning_macros::xtest;
306+ ///
307+ /// fn f1() {}
308+ ///
309+ /// #[xtest(feature = "_test_utils")]
310+ /// pub fn test_f1() {
311+ /// f1();
312+ /// }
313+ /// ```
314+ ///
315+ /// May also be applied to modules, like so:
316+ ///
317+ /// ```rust
318+ /// use lightning_macros::xtest;
319+ ///
320+ /// #[xtest(feature = "_test_utils")]
321+ /// pub mod tests {
322+ /// use super::*;
323+ ///
324+ /// fn f1() {}
325+ ///
326+ /// #[xtest]
327+ /// pub fn test_f1() {
328+ /// f1();
329+ /// }
330+ /// }
331+ /// ```
332+ ///
333+ /// Which will include the module if we are testing or the `_test_utils` feature
334+ /// is on.
335+ #[ proc_macro_attribute]
336+ pub fn xtest ( attrs : TokenStream , item : TokenStream ) -> TokenStream {
337+ let attrs = parse_macro_input ! ( attrs as TokenStream2 ) ;
338+ let input = parse_macro_input ! ( item as Item ) ;
339+
340+ let expanded = match input {
341+ Item :: Mod ( item_mod) => {
342+ let cfg = if attrs. is_empty ( ) {
343+ quote ! { #[ cfg_attr( test, test) ] }
344+ } else {
345+ quote ! { #[ cfg_attr( test, test) ] #[ cfg( any( test, #attrs) ) ] }
346+ } ;
347+ quote ! {
348+ #cfg
349+ #item_mod
350+ }
351+ } ,
352+ Item :: Fn ( item_fn) => {
353+ let ( cfg_attr, submit_attr) = if attrs. is_empty ( ) {
354+ ( quote ! { #[ cfg_attr( test, test) ] } , quote ! { #[ cfg( not( test) ) ] } )
355+ } else {
356+ (
357+ quote ! { #[ cfg_attr( test, test) ] #[ cfg( any( test, #attrs) ) ] } ,
358+ quote ! { #[ cfg( all( not( test) , #attrs) ) ] } ,
359+ )
360+ } ;
361+
362+ // Check that the function doesn't take args and returns nothing
363+ if !item_fn. sig . inputs . is_empty ( )
364+ || !matches ! ( item_fn. sig. output, syn:: ReturnType :: Default )
365+ {
366+ return syn:: Error :: new_spanned (
367+ item_fn. sig ,
368+ "xtest functions must not take arguments and must return nothing" ,
369+ )
370+ . to_compile_error ( )
371+ . into ( ) ;
372+ }
373+
374+ // Check for #[should_panic] attribute
375+ let should_panic =
376+ item_fn. attrs . iter ( ) . any ( |attr| attr. path ( ) . is_ident ( "should_panic" ) ) ;
377+
378+ let fn_name = & item_fn. sig . ident ;
379+ let fn_name_str = fn_name. to_string ( ) ;
380+ quote ! {
381+ #cfg_attr
382+ #item_fn
383+
384+ // We submit the test to the inventory only if we're not actually testing
385+ #submit_attr
386+ inventory:: submit! {
387+ crate :: XTestItem {
388+ test_fn: #fn_name,
389+ test_name: #fn_name_str,
390+ should_panic: #should_panic,
391+ }
392+ }
393+ }
394+ } ,
395+ _ => {
396+ return syn:: Error :: new_spanned (
397+ input,
398+ "xtest can only be applied to functions or modules" ,
399+ )
400+ . to_compile_error ( )
401+ . into ( ) ;
402+ } ,
403+ } ;
404+
405+ TokenStream :: from ( expanded)
406+ }
407+
408+ /// Collects all externalized tests marked with `#[xtest]`
409+ /// into a vector of `XTestItem`s. This vector can be
410+ /// retrieved by calling `get_xtests()`.
411+ #[ proc_macro]
412+ pub fn xtest_inventory ( _input : TokenStream ) -> TokenStream {
413+ let expanded = quote ! {
414+ /// An externalized test item, including the test function, name, and whether it is marked with `#[should_panic]`.
415+ pub struct XTestItem {
416+ pub test_fn: fn ( ) ,
417+ pub test_name: & ' static str ,
418+ pub should_panic: bool ,
419+ }
420+
421+ inventory:: collect!( XTestItem ) ;
422+
423+ pub fn get_xtests( ) -> Vec <& ' static XTestItem > {
424+ inventory:: iter:: <XTestItem >
425+ . into_iter( )
426+ . collect( )
427+ }
428+ } ;
429+
430+ TokenStream :: from ( expanded)
431+ }
0 commit comments