@@ -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,107 @@ 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 = "_externalize_tests")]
310+ /// pub fn test_f1() {
311+ /// f1();
312+ /// }
313+ /// ```
314+ ///
315+ /// Which will include the module if we are testing or the `_test_utils` feature
316+ /// is on.
317+ #[ proc_macro_attribute]
318+ pub fn xtest ( attrs : TokenStream , item : TokenStream ) -> TokenStream {
319+ let attrs = parse_macro_input ! ( attrs as TokenStream2 ) ;
320+ let input = parse_macro_input ! ( item as Item ) ;
321+
322+ let expanded = match input {
323+ Item :: Fn ( item_fn) => {
324+ let ( cfg_attr, submit_attr) = if attrs. is_empty ( ) {
325+ ( quote ! { #[ cfg_attr( test, test) ] } , quote ! { #[ cfg( not( test) ) ] } )
326+ } else {
327+ (
328+ quote ! { #[ cfg_attr( test, test) ] #[ cfg( any( test, #attrs) ) ] } ,
329+ quote ! { #[ cfg( all( not( test) , #attrs) ) ] } ,
330+ )
331+ } ;
332+
333+ // Check that the function doesn't take args and returns nothing
334+ if !item_fn. sig . inputs . is_empty ( )
335+ || !matches ! ( item_fn. sig. output, syn:: ReturnType :: Default )
336+ {
337+ return syn:: Error :: new_spanned (
338+ item_fn. sig ,
339+ "xtest functions must not take arguments and must return nothing" ,
340+ )
341+ . to_compile_error ( )
342+ . into ( ) ;
343+ }
344+
345+ // Check for #[should_panic] attribute
346+ let should_panic =
347+ item_fn. attrs . iter ( ) . any ( |attr| attr. path ( ) . is_ident ( "should_panic" ) ) ;
348+
349+ let fn_name = & item_fn. sig . ident ;
350+ let fn_name_str = fn_name. to_string ( ) ;
351+ quote ! {
352+ #cfg_attr
353+ #item_fn
354+
355+ // We submit the test to the inventory only if we're not actually testing
356+ #submit_attr
357+ inventory:: submit! {
358+ crate :: XTestItem {
359+ test_fn: #fn_name,
360+ test_name: #fn_name_str,
361+ should_panic: #should_panic,
362+ }
363+ }
364+ }
365+ } ,
366+ _ => {
367+ return syn:: Error :: new_spanned (
368+ input,
369+ "xtest can only be applied to functions or modules" ,
370+ )
371+ . to_compile_error ( )
372+ . into ( ) ;
373+ } ,
374+ } ;
375+
376+ TokenStream :: from ( expanded)
377+ }
378+
379+ /// Collects all externalized tests marked with `#[xtest]`
380+ /// into a vector of `XTestItem`s. This vector can be
381+ /// retrieved by calling `get_xtests()`.
382+ #[ proc_macro]
383+ pub fn xtest_inventory ( _input : TokenStream ) -> TokenStream {
384+ let expanded = quote ! {
385+ /// An externalized test item, including the test function, name, and whether it is marked with `#[should_panic]`.
386+ pub struct XTestItem {
387+ pub test_fn: fn ( ) ,
388+ pub test_name: & ' static str ,
389+ pub should_panic: bool ,
390+ }
391+
392+ inventory:: collect!( XTestItem ) ;
393+
394+ pub fn get_xtests( ) -> Vec <& ' static XTestItem > {
395+ inventory:: iter:: <XTestItem >
396+ . into_iter( )
397+ . collect( )
398+ }
399+ } ;
400+
401+ TokenStream :: from ( expanded)
402+ }
0 commit comments