use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Ident, ItemFn, LitInt, LitStr, Token,
};
#[derive(Default)]
struct PresentarTestAttrs {
fixture: Option<String>,
timeout_ms: u64,
should_panic: bool,
ignore: bool,
}
impl Parse for PresentarTestAttrs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut attrs = Self {
timeout_ms: 5000,
..Default::default()
};
while !input.is_empty() {
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
match ident_str.as_str() {
"fixture" => {
input.parse::<Token![=]>()?;
let lit: LitStr = input.parse()?;
attrs.fixture = Some(lit.value());
}
"timeout" => {
input.parse::<Token![=]>()?;
let lit: LitInt = input.parse()?;
attrs.timeout_ms = lit.base10_parse().unwrap_or(5000);
}
"should_panic" => {
attrs.should_panic = true;
}
"ignore" => {
attrs.ignore = true;
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!("unknown attribute: {ident_str}"),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(attrs)
}
}
#[proc_macro_attribute]
pub fn presentar_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attrs = parse_macro_input!(attr as PresentarTestAttrs);
let expanded = impl_presentar_test(&input, &attrs);
TokenStream::from(expanded)
}
fn impl_presentar_test(input: &ItemFn, attrs: &PresentarTestAttrs) -> TokenStream2 {
let _fn_name = &input.sig.ident;
let fn_body = &input.block;
let fn_attrs = &input.attrs;
let fn_vis = &input.vis;
let fn_sig = &input.sig;
let test_attr = if attrs.should_panic {
quote! { #[test] #[should_panic] }
} else {
quote! { #[test] }
};
let ignore_attr = if attrs.ignore {
quote! { #[ignore] }
} else {
quote! {}
};
let fixture_code = if let Some(fixture_path) = &attrs.fixture {
quote! {
let _fixture_data = include_bytes!(#fixture_path);
}
} else {
quote! {}
};
let timeout_ms = attrs.timeout_ms;
let timeout_code = quote! {
let _timeout_ms: u64 = #timeout_ms;
};
quote! {
#(#fn_attrs)*
#test_attr
#ignore_attr
#fn_vis #fn_sig {
#fixture_code
#timeout_code
#fn_body
}
}
}
#[proc_macro]
pub fn describe_suite(input: TokenStream) -> TokenStream {
let _input_str = input.to_string();
let expanded = quote! {
};
TokenStream::from(expanded)
}
#[proc_macro]
pub fn assert_snapshot(input: TokenStream) -> TokenStream {
let input2 = TokenStream2::from(input);
let expanded = quote! {
{
let (widget, name) = (#input2);
let snapshot = presentar_test::Snapshot::capture(&widget);
snapshot.assert_match(name);
}
};
TokenStream::from(expanded)
}
#[proc_macro]
pub fn fixture(input: TokenStream) -> TokenStream {
let input2 = TokenStream2::from(input);
let expanded = quote! {
#input2
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn interface_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let interface_name: Ident = parse_macro_input!(attr as Ident);
let _fn_name = &input.sig.ident;
let fn_body = &input.block;
let fn_attrs = &input.attrs;
let fn_vis = &input.vis;
let fn_sig = &input.sig;
let proof_type = Ident::new(
&format!("{interface_name}InterfaceProof"),
interface_name.span(),
);
let expanded = quote! {
#[allow(dead_code)]
pub struct #proof_type {
_private: (),
}
impl #proof_type {
#[cfg(test)]
pub const fn verified() -> Self {
Self { _private: () }
}
}
#(#fn_attrs)*
#[test]
#fn_vis #fn_sig {
let _proof = #proof_type { _private: () };
#fn_body
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn requires_interface(attr: TokenStream, item: TokenStream) -> TokenStream {
let interface_name: Ident = parse_macro_input!(attr as Ident);
let item2 = TokenStream2::from(item);
let proof_type = Ident::new(
&format!("{interface_name}InterfaceProof"),
interface_name.span(),
);
let expanded = quote! {
#[allow(dead_code)]
const _: () = {
fn _require_interface_test() {
let _ = core::mem::size_of::<#proof_type>();
}
};
#item2
};
TokenStream::from(expanded)
}
#[proc_macro]
pub fn computeblock(input: TokenStream) -> TokenStream {
let input_str = input.to_string();
if !input_str.contains("name:") || !input_str.contains("tests:") {
return TokenStream::from(quote! {
compile_error!(
"SPEC-024 ENFORCEMENT: computeblock! requires 'name:' and 'tests:' fields.\n\
TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS."
);
});
}
let expanded = quote! {
};
TokenStream::from(expanded)
}
#[cfg(test)]
mod tests {
}