Skip to content

Commit 87641ee

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 36ba27a commit 87641ee

File tree

2 files changed

+128
-2
lines changed

2 files changed

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

0 commit comments

Comments
 (0)