@@ -22,9 +22,11 @@ extern crate alloc;
22
22
23
23
use alloc:: string:: ToString ;
24
24
use proc_macro:: { Delimiter , Group , TokenStream , TokenTree } ;
25
+ use proc_macro2:: TokenStream as TokenStream2 ;
25
26
use quote:: quote;
26
27
use syn:: spanned:: Spanned ;
27
28
use syn:: { parse, ImplItemFn , Token } ;
29
+ use syn:: { parse_macro_input, Item } ;
28
30
29
31
fn add_async_method ( mut parsed : ImplItemFn ) -> TokenStream {
30
32
let output = quote ! {
@@ -294,3 +296,107 @@ pub fn drop_legacy_field_definition(expr: TokenStream) -> TokenStream {
294
296
let out = syn:: Expr :: Struct ( st) ;
295
297
quote ! { #out } . into ( )
296
298
}
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