Skip to content

Commit 4c05726

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 047bd61 commit 4c05726

File tree

2 files changed

+140
-2
lines changed

2 files changed

+140
-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: 135 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,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

Comments
 (0)