@@ -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,136 @@ 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 = "_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