Skip to content

Commit adf2f68

Browse files
committed
Introduce xtest macro for externally-visible tests
We want to allow functional tests to be run by other project, allowing them to replace components, such as the signer.
1 parent 5099dce commit adf2f68

File tree

2 files changed

+111
-2
lines changed

2 files changed

+111
-2
lines changed

lightning-macros/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ proc-macro = true
1818
[features]
1919

2020
[dependencies]
21-
syn = { version = "2.0.77", default-features = false, features = ["parsing", "printing", "proc-macro", "full"] }
22-
proc-macro2 = { version = "1.0.86", default-features = false, features = ["proc-macro"] }
21+
syn = { version = "2.0", default-features = false, features = ["parsing", "printing", "proc-macro", "full"] }
22+
proc-macro2 = { version = "1.0", default-features = false, features = ["proc-macro"] }
2323
quote = { version = "1.0", default-features = false, features = ["proc-macro"] }
2424

25+
[dev-dependencies]
26+
inventory = "0.3"
27+
2528
[lints]
2629
workspace = true

lightning-macros/src/lib.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ extern crate alloc;
2222

2323
use alloc::string::ToString;
2424
use proc_macro::{Delimiter, Group, TokenStream, TokenTree};
25+
use proc_macro2::TokenStream as TokenStream2;
2526
use quote::quote;
2627
use syn::spanned::Spanned;
2728
use syn::{parse, ImplItemFn, Token};
29+
use syn::{parse_macro_input, Item};
2830

2931
fn 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 = "_test_utils")]
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

Comments
 (0)