Skip to content

Commit e80df9c

Browse files
committed
macro for externally-visible tests
1 parent 8d8b4ea commit e80df9c

File tree

2 files changed

+143
-2
lines changed

2 files changed

+143
-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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
#![deny(rustdoc::private_intra_doc_links)]
1919
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2020

21+
extern crate alloc;
22+
23+
use alloc::string::ToString;
2124
use proc_macro::TokenStream;
25+
use proc_macro2::TokenStream as TokenStream2;
2226
use quote::quote;
2327
use syn::spanned::Spanned;
2428
use syn::{parse, ImplItemFn, Token};
29+
use syn::{parse_macro_input, Item};
2530

2631
fn add_async_method(mut parsed: ImplItemFn) -> TokenStream {
2732
let output = quote! {
@@ -74,3 +79,136 @@ pub fn maybe_await(expr: TokenStream) -> TokenStream {
7479

7580
quoted.into()
7681
}
82+
83+
/// An exposed test. This is a test that will run locally and also be
84+
/// made available to other crates that want to run it in their own context.
85+
///
86+
/// For example:
87+
/// ```rust
88+
/// use lightning_macros::xtest;
89+
///
90+
/// fn f1() {}
91+
///
92+
/// #[xtest(feature = "_test_utils")]
93+
/// pub fn test_f1() {
94+
/// f1();
95+
/// }
96+
/// ```
97+
///
98+
/// May also be applied to modules, like so:
99+
///
100+
/// ```rust
101+
/// use lightning_macros::xtest;
102+
///
103+
/// #[xtest(feature = "_test_utils")]
104+
/// pub mod tests {
105+
/// use super::*;
106+
///
107+
/// fn f1() {}
108+
///
109+
/// #[xtest]
110+
/// pub fn test_f1() {
111+
/// f1();
112+
/// }
113+
/// }
114+
/// ```
115+
///
116+
/// Which will include the module if we are testing or the `_test_utils` feature
117+
/// is on.
118+
#[proc_macro_attribute]
119+
pub fn xtest(attrs: TokenStream, item: TokenStream) -> TokenStream {
120+
let attrs = parse_macro_input!(attrs as TokenStream2);
121+
let input = parse_macro_input!(item as Item);
122+
123+
let expanded = match input {
124+
Item::Mod(item_mod) => {
125+
let cfg = if attrs.is_empty() {
126+
quote! { #[cfg_attr(test, test)] }
127+
} else {
128+
quote! { #[cfg_attr(test, test)] #[cfg(any(test, #attrs))] }
129+
};
130+
quote! {
131+
#cfg
132+
#item_mod
133+
}
134+
},
135+
Item::Fn(item_fn) => {
136+
let (cfg_attr, submit_attr) = if attrs.is_empty() {
137+
(quote! { #[cfg_attr(test, test)] }, quote! { #[cfg(not(test))] })
138+
} else {
139+
(
140+
quote! { #[cfg_attr(test, test)] #[cfg(any(test, #attrs))] },
141+
quote! { #[cfg(all(not(test), #attrs))] },
142+
)
143+
};
144+
145+
// Check that the function doesn't take args and returns nothing
146+
if !item_fn.sig.inputs.is_empty()
147+
|| !matches!(item_fn.sig.output, syn::ReturnType::Default)
148+
{
149+
return syn::Error::new_spanned(
150+
item_fn.sig,
151+
"xtest functions must not take arguments and must return nothing",
152+
)
153+
.to_compile_error()
154+
.into();
155+
}
156+
157+
// Check for #[should_panic] attribute
158+
let should_panic =
159+
item_fn.attrs.iter().any(|attr| attr.path().is_ident("should_panic"));
160+
161+
let fn_name = &item_fn.sig.ident;
162+
let fn_name_str = fn_name.to_string();
163+
quote! {
164+
#cfg_attr
165+
#item_fn
166+
167+
// We submit the test to the inventory only if we're not actually testing
168+
#submit_attr
169+
inventory::submit! {
170+
crate::XTestItem {
171+
test_fn: #fn_name,
172+
test_name: #fn_name_str,
173+
should_panic: #should_panic,
174+
}
175+
}
176+
}
177+
},
178+
_ => {
179+
return syn::Error::new_spanned(
180+
input,
181+
"xtest can only be applied to functions or modules",
182+
)
183+
.to_compile_error()
184+
.into();
185+
},
186+
};
187+
188+
TokenStream::from(expanded)
189+
}
190+
191+
/// Collects all externalized tests marked with `#[xtest]`
192+
/// into a vector of `XTestItem`s. This vector can be
193+
/// retrieved by calling `get_xtests()`.
194+
#[proc_macro]
195+
pub fn xtest_inventory(_input: TokenStream) -> TokenStream {
196+
let expanded = quote! {
197+
/// An externalized test item, including the test function, name, and whether it is marked with `#[should_panic]`.
198+
pub struct XTestItem {
199+
pub test_fn: fn(),
200+
pub test_name: &'static str,
201+
pub should_panic: bool,
202+
}
203+
204+
inventory::collect!(XTestItem);
205+
206+
pub fn get_xtests() -> Vec<&'static XTestItem> {
207+
inventory::iter::<XTestItem>
208+
.into_iter()
209+
.collect()
210+
}
211+
};
212+
213+
TokenStream::from(expanded)
214+
}

0 commit comments

Comments
 (0)