mod ast_tests;
mod cst_tests;
mod error_tests;
mod test_macros;
use {
crate::{
Args,
process,
},
ferrotype::Ferrotype,
quote::quote,
};
#[test_log::test]
fn test_macro_attribute_parsing() {
let args = Args::parse(quote! { AST });
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(!args.is_error_node());
assert!(!args.has_semantic_tokens());
let args = Args::parse(quote! { CST });
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(!args.is_error_node());
assert!(!args.has_semantic_tokens());
let args = Args::parse(quote! { AST, error });
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.is_error_node());
assert!(!args.has_semantic_tokens());
let args = Args::parse(quote! { CST, error });
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(args.is_error_node());
assert!(!args.has_semantic_tokens());
let args = Args::parse(quote! { AST, allow_semantic });
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(!args.is_error_node());
assert!(args.has_semantic_tokens());
let args = Args::parse(quote! { CST, allow_semantic });
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(!args.is_error_node());
assert!(args.has_semantic_tokens());
let args = Args::parse(quote! { AST, error, allow_semantic });
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.is_error_node());
assert!(args.has_semantic_tokens());
let args = Args::parse(quote! { CST, error, allow_semantic });
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(args.is_error_node());
assert!(args.has_semantic_tokens());
}
#[test_log::test]
fn test_real_world_ast_example() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(AST)]
pub struct Function {
visibility: Field<Enum<crate::modifier::Visibility>>,
ident: NodeId<crate::symbol::Ident>,
generic_parameters: Option<NodeId<crate::ty::GenericParameters>>,
parameters: Vec<crate::ty::Parameters>,
return_ty: Option<NodeId<crate::ty::Path>>,
effects_required: Vec<NodeId<crate::ty::Path>>,
effects_handled: Vec<NodeId<crate::ty::Path>>,
body: NodeId<crate::expression::Block>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
let output_str = output.to_string();
assert!(output_str.contains("impl Function"));
assert!(output_str.contains("get_visibility"));
assert!(output_str.contains("get_ident"));
assert!(output_str.contains("get_body"));
},
| Err(e) => {
panic!("Real-world example failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_reserved_keyword_field_name() {
let input = quote! {
#[laburnum_syntax(AST)]
pub struct BadFieldName {
self_: NodeId<crate::Type>,
}
};
match process(quote!(AST), input) {
| Ok(_) => {
panic!("Expected ReservedKeywordFieldName error");
},
| Err(e) => {
let error_message = e.to_string();
assert!(
error_message.contains("self_") && error_message.contains("Self"),
"Error message should mention 'self_' and 'Self', got: {error_message}",
);
},
}
}
#[test_log::test]
fn test_real_world_cst_example() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(CST)]
pub struct FunctionDeclaration {
span: Span,
pub_token: Option<NodeId<crate::Token>>,
fn_token: NodeId<crate::Token>,
identifier: NodeId<crate::Identifier>,
generic_params: Option<NodeId<crate::GenericParameters>>,
lparen: NodeId<crate::Token>,
params: Vec<NodeId<crate::Parameter>>,
rparen: NodeId<crate::Token>,
return_type: Option<NodeId<crate::ReturnType>>,
body: NodeId<crate::BlockExpression>,
trivia: Vec<crate::Trivia>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(CST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
let output_str = output.to_string();
assert!(output_str.contains("impl FunctionDeclaration"));
},
| Err(e) => {
panic!("CST example failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_complex_enum_field() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(AST)]
pub struct ComplexEnum {
visibility: Field<Enum<crate::modifier::Visibility>>,
expr: EnumNodeId<crate::expression::Inline>,
maybe_expr: Option<EnumNodeId<crate::expression::Expression>>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
},
| Err(e) => {
panic!("Complex enum test failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_edge_case_many_union_types() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(AST)]
pub struct ManyUnionTypes {
two_types: NodeId<crate::Type1, crate::Type2>,
three_types: NodeId<crate::Type1, crate::Type2, crate::Type3>,
four_types: NodeId<crate::Type1, crate::Type2, crate::Type3, crate::Type4>,
vec_multi: Vec<crate::declaration::Function, crate::declaration::Import>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
},
| Err(e) => {
panic!("Many union types test failed: {e:?}");
},
}
snapshot.print_stdout();
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_deeply_nested_types() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(AST)]
pub struct DeeplyNested {
nested1: Option<Vec<NodeId<crate::Type>>>,
nested2: Vec<Option<NodeId<crate::Type>>>,
nested3: Option<Vec<crate::declaration::Function, crate::declaration::Import>>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
},
| Err(e) => {
panic!("Deeply nested types test failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_attributes_preserved() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[derive(Debug, Clone, PartialEq)]
#[cfg(feature = "serde")]
#[laburnum_syntax(AST)]
pub struct PreserveAttributes {
#[serde(rename = "customName")]
field: NodeId<crate::Type>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
let output_str = output.to_string();
assert!(output_str.contains("derive"));
assert!(output_str.contains("Debug"));
assert!(output_str.contains("Clone"));
},
| Err(e) => {
panic!("Preserve attributes test failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}
#[test_log::test]
fn test_args_parsing_with_spans() {
let cst_tokens = quote! { CST };
let args = Args::parse(cst_tokens.clone());
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(args.cst_span.is_some());
assert!(args.ast_span.is_none());
let ast_tokens = quote! { AST };
let args = Args::parse(ast_tokens.clone());
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.ast_span.is_some());
assert!(args.cst_span.is_none());
let multi_tokens = quote! { CST, error, allow_semantic };
let args = Args::parse(multi_tokens);
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(args.is_error_node());
assert!(args.has_semantic_tokens());
assert!(args.cst_span.is_some());
assert!(args.ast_span.is_none());
}
#[test_log::test]
fn test_args_parsing_edge_cases() {
let empty_tokens = quote! {};
let args = Args::parse(empty_tokens);
assert!(!args.is_ast());
assert!(!args.is_cst());
assert!(!args.is_error_node());
assert!(!args.has_semantic_tokens());
assert!(args.cst_span.is_none());
assert!(args.ast_span.is_none());
let extra_comma_tokens = quote! { AST, };
let args = Args::parse(extra_comma_tokens);
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.ast_span.is_some());
let whitespace_tokens = quote! { CST , error };
let args = Args::parse(whitespace_tokens);
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(args.is_error_node());
assert!(args.cst_span.is_some());
let unknown_tokens = quote! { AST, unknown_flag, error };
let args = Args::parse(unknown_tokens);
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.is_error_node());
assert!(!args.has_semantic_tokens());
}
#[test_log::test]
fn test_args_parsing_complex_semantic_patterns() {
let complex_tokens = quote! { CST, SemanticTokenType };
let args = Args::parse(complex_tokens);
assert!(!args.is_ast());
assert!(args.is_cst());
assert!(!args.is_error_node());
assert!(!args.has_semantic_tokens());
assert!(args.cst_span.is_some());
let path_tokens = quote! { AST, SomeModule::SomeType };
let args = Args::parse(path_tokens);
assert!(args.is_ast());
assert!(!args.is_cst());
assert!(args.ast_span.is_some());
}
#[test_log::test]
fn test_args_span_accuracy() {
let tokens = quote! { AST, error };
let args = Args::parse(tokens.clone());
assert!(args.is_ast());
assert!(args.is_error_node());
assert!(args.ast_span.is_some());
let ast_span = args.ast_span.unwrap();
assert!(!format!("{ast_span:?}").is_empty());
}
#[test_log::test]
fn test_args_parsing_all_combinations() {
let test_cases = vec![
(quote! { AST }, true, false, false, false),
(quote! { CST }, false, true, false, false),
(quote! { AST, error }, true, false, true, false),
(quote! { CST, error }, false, true, true, false),
(quote! { AST, allow_semantic }, true, false, false, true),
(quote! { CST, allow_semantic }, false, true, false, true),
(
quote! { AST, error, allow_semantic },
true,
false,
true,
true,
),
(
quote! { CST, error, allow_semantic },
false,
true,
true,
true,
),
];
for (tokens, expect_ast, expect_cst, expect_error, expect_semantic) in
test_cases
{
let args = Args::parse(tokens);
assert_eq!(args.is_ast(), expect_ast, "AST flag mismatch");
assert_eq!(args.is_cst(), expect_cst, "CST flag mismatch");
assert_eq!(args.is_error_node(), expect_error, "error flag mismatch");
assert_eq!(
args.has_semantic_tokens(),
expect_semantic,
"semantic flag mismatch"
);
if expect_ast {
assert!(args.ast_span.is_some(), "AST span should be tracked");
} else {
assert!(args.ast_span.is_none(), "AST span should not be tracked");
}
if expect_cst {
assert!(args.cst_span.is_some(), "CST span should be tracked");
} else {
assert!(args.cst_span.is_none(), "CST span should not be tracked");
}
}
}
#[test_log::test]
fn test_process_function_uses_correct_spans() {
let conflicting_tokens = quote! { AST, CST };
let args = Args::parse(conflicting_tokens.clone());
assert!(args.ast_span.is_some());
assert!(args.cst_span.is_some());
assert!(args.is_ast());
assert!(args.is_cst());
let dummy_item = quote! {
pub struct TestStruct {
field: NodeId<crate::Type>,
}
};
match process(conflicting_tokens, dummy_item) {
| Ok(_) => panic!("Expected error for conflicting flags"),
| Err(e) => {
assert!(e.to_string().contains("Cannot specify both AST and CST"));
},
}
}
#[test_log::test]
fn test_macro_hygiene() {
let mut snapshot = Ferrotype::new();
let input = quote! {
#[laburnum_syntax(AST)]
pub struct HygieneTest {
my_self: NodeId<crate::Type>,
ast: NodeId<crate::AST>,
node: NodeId<crate::Node>,
field: Field<bool>,
my_type: NodeId<crate::Type>,
my_impl: Option<NodeId<crate::Type>>,
}
};
snapshot.add_token_stream("Input", &input);
match process(quote!(AST), input) {
| Ok(output) => {
snapshot.add_token_stream("Output", &output);
let output_str = output.to_string();
assert!(output_str.contains("impl HygieneTest"));
},
| Err(e) => {
panic!("Macro hygiene test failed: {e:?}");
},
}
ferrotype::assert!(snapshot);
}