use proptest::{ prelude::*, strategy::BoxedStrategy, }; use crate::syntax::{ lexer::{ Lexer, Token, }, FileParser, }; // ===== Generators for valid syntax elements ===== fn valid_ident() -> impl Strategy { "[a-zA-Z_][a-zA-Z0-9_]{0,20}".prop_filter("not a keyword", |s| { !matches!( s.as_str(), "use" | "character" | "template" | "life_arc" | "schedule" | "behavior" | "institution" | "relationship" | "location" | "species" | "enum" | "state" | "on" | "as" | "self" | "other" | "remove" | "append" | "forall" | "exists" | "in" | "where" | "and" | "or" | "not" | "is" | "true" | "false" ) }) } fn valid_string() -> impl Strategy { // Strings without quotes or backslashes for simplicity "[a-zA-Z0-9 ,.!?-]{0,50}" } fn valid_int() -> impl Strategy { -1000i64..1000i64 } fn valid_float() -> impl Strategy { (-1000.0..1000.0).prop_filter("finite", |f: &f64| f.is_finite()) } fn valid_time() -> impl Strategy { (0u8..24, 0u8..60, 0u8..60) } fn valid_duration() -> impl Strategy { (0u32..24, 0u32..60, 0u32..60) } // ===== Lexer property tests ===== proptest! { #[test] fn test_lexer_doesnt_panic(s in "\\PC{0,100}") { // Any string should not panic the lexer let lexer = Lexer::new(&s); let _tokens: Vec<_> = lexer.collect(); } #[test] fn test_valid_ident_tokenizes(name in valid_ident()) { let lexer = Lexer::new(&name); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match &tokens[0] { Token::Ident(s) => assert_eq!(s, &name), _ => panic!("Expected Ident token, got {:?}", tokens[0]), } } #[test] fn test_valid_int_tokenizes(n in valid_int()) { let input = n.to_string(); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match tokens[0] { Token::IntLit(val) => assert_eq!(val, n), _ => panic!("Expected IntLit token"), } } #[test] fn test_valid_float_tokenizes(n in valid_float()) { let input = format!("{:.2}", n); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match tokens[0] { Token::FloatLit(_) => {}, _ => panic!("Expected FloatLit token"), } } #[test] fn test_valid_string_tokenizes(s in valid_string()) { let input = format!("\"{}\"", s); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match &tokens[0] { Token::StringLit(val) => assert_eq!(val, &s), _ => panic!("Expected StringLit token"), } } #[test] fn test_time_literal_tokenizes(time in valid_time()) { let (h, m, s) = time; let input = format!("{:02}:{:02}:{:02}", h, m, s); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match &tokens[0] { Token::TimeLit(_) => {}, _ => panic!("Expected TimeLit token"), } } #[test] fn test_duration_literal_tokenizes(dur in valid_duration()) { let (h, m, s) = dur; let input = if h > 0 && m > 0 && s > 0 { format!("{}h{}m{}s", h, m, s) } else if h > 0 && m > 0 { format!("{}h{}m", h, m) } else if h > 0 { format!("{}h", h) } else if m > 0 { format!("{}m", m) } else { format!("{}s", s) }; let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); if !input.is_empty() && input != "0h" && input != "0m" && input != "0s" { assert!(!tokens.is_empty(), "Duration '{}' should tokenize", input); } } #[test] fn test_keywords_are_distinct_from_idents( keyword in prop::sample::select(vec![ "character", "template", "enum", "use", "self", "other", "and", "or", "not", "is", "true", "false" ]) ) { let lexer = Lexer::new(keyword); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); // Should be a keyword token, not Ident if let Token::Ident(_) = &tokens[0] { panic!("'{}' should be a keyword, not an Ident", keyword) } } #[test] fn test_whitespace_separates_tokens( name1 in valid_ident(), name2 in valid_ident(), ws in "[ \t\n]{1,5}" ) { let input = format!("{}{}{}", name1, ws, name2); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 2); match (&tokens[0], &tokens[1]) { (Token::Ident(s1), Token::Ident(s2)) => { assert_eq!(s1, &name1); assert_eq!(s2, &name2); } _ => panic!("Expected two Ident tokens"), } } } // ===== Parser property tests ===== fn valid_field() -> impl Strategy { (valid_ident(), valid_int().prop_map(|n| n.to_string())) } fn valid_character() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_field(), 0..5)).prop_map(|(name, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!("character {} {{\n{}\n}}", name, fields_str) }) } fn valid_template() -> impl Strategy { ( valid_ident(), prop::collection::vec( (valid_ident(), valid_int(), valid_int()).prop_map(|(name, lo, hi)| { let (min, max) = if lo < hi { (lo, hi) } else { (hi, lo) }; (name, format!("{}..{}", min, max)) }), 0..5, ), ) .prop_map(|(name, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!("template {} {{\n{}\n}}", name, fields_str) }) } fn valid_enum() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_ident(), 1..10)) .prop_map(|(name, variants)| format!("enum {} {{ {} }}", name, variants.join(", "))) } fn valid_schedule() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_time(), 1..5)).prop_map(|(name, times)| { let blocks = times .windows(2) .map(|w| { let (h1, m1, s1) = w[0]; let (h2, m2, _) = w[1]; format!( " {:02}:{:02}:{:02} -> {:02}:{:02}:00: activity", h1, m1, s1, h2, m2 ) }) .collect::>() .join("\n"); format!("schedule {} {{\n{}\n}}", name, blocks) }) } fn valid_location() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_field(), 0..5)).prop_map(|(name, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!("location {} {{\n{}\n}}", name, fields_str) }) } fn valid_species() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_field(), 0..5)).prop_map(|(name, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!("species {} {{\n{}\n}}", name, fields_str) }) } fn valid_institution() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_field(), 0..5)).prop_map(|(name, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!("institution {} {{\n{}\n}}", name, fields_str) }) } fn valid_relationship() -> impl Strategy { ( valid_ident(), valid_ident(), valid_ident(), prop::collection::vec(valid_field(), 0..3), ) .prop_map(|(name, person1, person2, fields)| { let fields_str = fields .iter() .map(|(k, v)| format!(" {}: {}", k, v)) .collect::>() .join("\n"); format!( "relationship {} {{\n {}\n {}\n{}\n}}", name, person1, person2, fields_str ) }) } proptest! { #[test] fn test_parser_doesnt_panic(input in "\\PC{0,200}") { let lexer = Lexer::new(&input); let parser = FileParser::new(); let _ = parser.parse(lexer); // Should not panic } #[test] fn test_valid_character_parses(input in valid_character()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid character: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Character(_) => {}, _ => panic!("Expected Character declaration"), } } } #[test] fn test_valid_template_parses(input in valid_template()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid template: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Template(_) => {}, _ => panic!("Expected Template declaration"), } } } #[test] fn test_valid_enum_parses(input in valid_enum()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid enum: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Enum(_) => {}, _ => panic!("Expected Enum declaration"), } } } #[test] fn test_valid_schedule_parses(input in valid_schedule()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid schedule: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Schedule(_) => {}, _ => panic!("Expected Schedule declaration"), } } } #[test] fn test_valid_location_parses(input in valid_location()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid location: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Location(_) => {}, _ => panic!("Expected Location declaration"), } } } #[test] fn test_valid_species_parses(input in valid_species()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid species: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Species(_) => {}, _ => panic!("Expected Species declaration"), } } } #[test] fn test_valid_institution_parses(input in valid_institution()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid institution: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Institution(_) => {}, _ => panic!("Expected Institution declaration"), } } } #[test] fn test_valid_relationship_parses(input in valid_relationship()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid relationship: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Relationship(_) => {}, _ => panic!("Expected Relationship declaration"), } } } #[test] fn test_multiple_declarations_parse( chars in prop::collection::vec(valid_character(), 0..3), templates in prop::collection::vec(valid_template(), 0..3), enums in prop::collection::vec(valid_enum(), 0..3), ) { let mut all = chars; all.extend(templates); all.extend(enums); let input = all.join("\n\n"); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); if !all.is_empty() { assert!(result.is_ok(), "Failed to parse multiple declarations:\n{}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), all.len()); } } } } // ===== Life Arc generators ===== fn valid_comparison_expr() -> impl Strategy { ( valid_ident(), prop::sample::select(vec![">", ">=", "<", "<="]), prop_oneof![ valid_int().prop_map(|n| n.to_string()), valid_float().prop_map(|f| format!("{:.2}", f)), ], ) .prop_map(|(ident, op, val)| format!("{} {} {}", ident, op, val)) } fn valid_equality_expr() -> impl Strategy { ( valid_ident(), prop_oneof![ valid_int().prop_map(|n| n.to_string()), valid_float().prop_map(|f| format!("{:.2}", f)), valid_string().prop_map(|s| format!("\"{}\"", s)), Just("true".to_string()), Just("false".to_string()), ], ) .prop_map(|(ident, val)| format!("{} is {}", ident, val)) } fn valid_logical_and_expr() -> impl Strategy { (valid_comparison_expr(), valid_comparison_expr()) .prop_map(|(left, right)| format!("{} and {}", left, right)) } fn valid_logical_or_expr() -> impl Strategy { (valid_ident(), valid_ident()).prop_map(|(left, right)| format!("{} or {}", left, right)) } fn valid_logical_not_expr() -> impl Strategy { valid_ident().prop_map(|ident| format!("not {}", ident)) } fn valid_field_access_expr() -> impl Strategy { (prop::sample::select(vec!["self", "other"]), valid_ident()) .prop_map(|(base, field)| format!("{}.{}", base, field)) } fn valid_field_access_comparison() -> impl Strategy { ( valid_field_access_expr(), prop::sample::select(vec![">", ">=", "<", "<="]), prop_oneof![valid_int().prop_map(|n| n.to_string()), valid_ident(),], ) .prop_map(|(field, op, val)| format!("{} {} {}", field, op, val)) } fn valid_transition_condition() -> impl Strategy { prop_oneof![ valid_ident(), // Simple identifier valid_int().prop_map(|n| n.to_string()), // Literal int Just("true".to_string()), // Boolean literal Just("false".to_string()), valid_comparison_expr(), // Comparison expression valid_equality_expr(), // Equality expression valid_logical_and_expr(), // Logical AND valid_logical_or_expr(), // Logical OR valid_logical_not_expr(), // Logical NOT valid_field_access_expr(), // Field access valid_field_access_comparison(), // Field access with comparison ] } fn valid_transition() -> impl Strategy { (valid_transition_condition(), valid_ident()) .prop_map(|(cond, target)| format!(" on {} -> {}", cond, target)) } fn valid_arc_state() -> impl Strategy { ( valid_ident(), prop::collection::vec(valid_transition(), 0..3), ) .prop_map(|(state_name, transitions)| { let trans_str = transitions.join("\n"); if transitions.is_empty() { format!(" state {} {{}}", state_name) } else { format!(" state {} {{\n{}\n }}", state_name, trans_str) } }) } fn valid_life_arc() -> impl Strategy { ( valid_ident(), prop::collection::vec(valid_arc_state(), 1..5), ) .prop_map(|(name, states)| { let states_str = states.join("\n"); format!("life_arc {} {{\n{}\n}}", name, states_str) }) } // ===== Behavior Tree generators ===== fn valid_action_node() -> impl Strategy { ( valid_ident(), prop::option::of(prop::collection::vec(valid_field(), 0..3)), ) .prop_map(|(name, params)| match params { | None => name, | Some(params) if params.is_empty() => format!("{}()", name), | Some(params) => { let params_str = params .iter() .map(|(k, v)| format!("{}: {}", k, v)) .collect::>() .join(", "); format!("{}({})", name, params_str) }, }) } fn valid_behavior_node_depth(depth: u32) -> BoxedStrategy { if depth == 0 { // Base case: just actions or subtrees prop_oneof![ valid_action_node(), valid_ident().prop_map(|name| format!("@{}", name)), ] .boxed() } else { // Recursive case: can be action, subtree, selector, or sequence let action = valid_action_node(); let subtree = valid_ident().prop_map(|name| format!("@{}", name)); let selector = prop::collection::vec(valid_behavior_node_depth(depth - 1), 1..3).prop_map( |children| { let children_str = children .iter() .map(|c| format!(" {}", c)) .collect::>() .join("\n"); format!("? {{\n{}\n }}", children_str) }, ); let sequence = prop::collection::vec(valid_behavior_node_depth(depth - 1), 1..3).prop_map( |children| { let children_str = children .iter() .map(|c| format!(" {}", c)) .collect::>() .join("\n"); format!("> {{\n{}\n }}", children_str) }, ); prop_oneof![action, subtree, selector, sequence,].boxed() } } fn valid_behavior_tree() -> impl Strategy { ( valid_ident(), valid_behavior_node_depth(2), // Max depth 2 to keep tests fast ) .prop_map(|(name, root)| format!("behavior {} {{\n {}\n}}", name, root)) } proptest! { #[test] fn test_valid_life_arc_parses(input in valid_life_arc()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid life_arc: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::LifeArc(_) => {}, _ => panic!("Expected LifeArc declaration"), } } } #[test] fn test_valid_behavior_tree_parses(input in valid_behavior_tree()) { let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse valid behavior: {}\nError: {:?}", input, result.err()); if let Ok(file) = result { assert_eq!(file.declarations.len(), 1); match &file.declarations[0] { crate::syntax::ast::Declaration::Behavior(_) => {}, _ => panic!("Expected Behavior declaration"), } } } // ===== Comprehensive edge case tests ===== #[test] fn test_life_arc_with_no_transitions(name in valid_ident(), state_name in valid_ident()) { let input = format!("life_arc {} {{\n state {} {{}}\n}}", name, state_name); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse life arc with no transitions: {:?}", result.err()); } #[test] fn test_life_arc_with_multiple_transitions( name in valid_ident(), state_name in valid_ident(), targets in prop::collection::vec(valid_ident(), 2..5) ) { let transitions = targets.iter() .map(|target| format!(" on ready -> {}", target)) .collect::>() .join("\n"); let input = format!("life_arc {} {{\n state {} {{\n{}\n }}\n}}", name, state_name, transitions); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse life arc with multiple transitions: {:?}", result.err()); } #[test] fn test_behavior_tree_deeply_nested(name in valid_ident()) { let input = format!( "behavior {} {{\n > {{\n ? {{\n > {{\n action\n }}\n }}\n }}\n}}", name ); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse deeply nested behavior tree: {:?}", result.err()); } #[test] fn test_behavior_tree_with_action_params( name in valid_ident(), action in valid_ident(), params in prop::collection::vec(valid_field(), 1..4) ) { let params_str = params.iter() .map(|(k, v)| format!("{}: {}", k, v)) .collect::>() .join(", "); let input = format!("behavior {} {{\n {}({})\n}}", name, action, params_str); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse behavior with action params: {:?}", result.err()); } #[test] fn test_behavior_tree_with_subtree_reference( name in valid_ident(), subtree_path in prop::collection::vec(valid_ident(), 1..3) ) { let path = subtree_path.join("::"); let input = format!("behavior {} {{\n @{}\n}}", name, path); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse behavior with subtree: {:?}", result.err()); } #[test] fn test_behavior_selector_with_many_children( name in valid_ident(), children in prop::collection::vec(valid_ident(), 2..10) ) { let children_str = children.iter() .map(|c| format!(" {}", c)) .collect::>() .join("\n"); let input = format!("behavior {} {{\n ? {{\n{}\n }}\n}}", name, children_str); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse selector with many children: {:?}", result.err()); } #[test] fn test_behavior_sequence_with_many_children( name in valid_ident(), children in prop::collection::vec(valid_ident(), 2..10) ) { let children_str = children.iter() .map(|c| format!(" {}", c)) .collect::>() .join("\n"); let input = format!("behavior {} {{\n > {{\n{}\n }}\n}}", name, children_str); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse sequence with many children: {:?}", result.err()); } #[test] fn test_life_arc_transition_with_literal_condition( name in valid_ident(), state_name in valid_ident(), target in valid_ident(), val in valid_int() ) { let input = format!("life_arc {} {{\n state {} {{\n on {} -> {}\n }}\n}}", name, state_name, val, target); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse life arc with literal condition: {:?}", result.err()); } #[test] fn test_life_arc_transition_with_bool_condition( name in valid_ident(), state_name in valid_ident(), target in valid_ident(), val in prop::sample::select(vec![true, false]) ) { let input = format!("life_arc {} {{\n state {} {{\n on {} -> {}\n }}\n}}", name, state_name, val, target); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse life arc with bool condition: {:?}", result.err()); } // ===== Comparison expression tests ===== #[test] fn test_comparison_all_operators( ident in valid_ident(), val in valid_int() ) { for op in &[">", ">=", "<", "<="] { let comp = format!("{} {} {}", ident, op, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse comparison '{}': {:?}", comp, result.err()); } } #[test] fn test_comparison_with_int(comp in valid_comparison_expr()) { let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse comparison '{}': {:?}", comp, result.err()); } #[test] fn test_comparison_gt(ident in valid_ident(), val in valid_int()) { let comp = format!("{} > {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse > comparison: {:?}", result.err()); } #[test] fn test_comparison_gte(ident in valid_ident(), val in valid_int()) { let comp = format!("{} >= {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse >= comparison: {:?}", result.err()); } #[test] fn test_comparison_lt(ident in valid_ident(), val in valid_int()) { let comp = format!("{} < {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse < comparison: {:?}", result.err()); } #[test] fn test_comparison_lte(ident in valid_ident(), val in valid_int()) { let comp = format!("{} <= {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse <= comparison: {:?}", result.err()); } #[test] fn test_comparison_with_float( ident in valid_ident(), val in valid_float() ) { let comp = format!("{} > {:.2}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse float comparison: {:?}", result.err()); } #[test] fn test_comparison_with_negative_int( ident in valid_ident(), val in -100i64..0i64 ) { let comp = format!("{} < {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse comparison with negative: {:?}", result.err()); } #[test] fn test_multiple_transitions_with_comparisons( name in valid_ident(), state_name in valid_ident(), comparisons in prop::collection::vec( (valid_ident(), prop::sample::select(vec![">", "<", ">=", "<="]), valid_int(), valid_ident()), 2..5 ) ) { let transitions = comparisons.iter() .map(|(var, op, val, target)| format!(" on {} {} {} -> {}", var, op, val, target)) .collect::>() .join("\n"); let input = format!("life_arc {} {{\n state {} {{\n{}\n }}\n}}", name, state_name, transitions); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse multiple comparison transitions: {:?}", result.err()); } // ===== Equality expression tests ===== #[test] fn test_equality_with_string( ident in valid_ident(), val in valid_string() ) { let comp = format!("{} is \"{}\"", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse equality with string: {:?}", result.err()); } #[test] fn test_equality_with_int( ident in valid_ident(), val in valid_int() ) { let comp = format!("{} is {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse equality with int: {:?}", result.err()); } #[test] fn test_equality_with_float( ident in valid_ident(), val in valid_float() ) { let comp = format!("{} is {:.2}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse equality with float: {:?}", result.err()); } #[test] fn test_equality_with_bool( ident in valid_ident(), val in prop::sample::select(vec![true, false]) ) { let comp = format!("{} is {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", comp); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse equality with bool: {:?}", result.err()); } #[test] fn test_equality_multiple_transitions( name in valid_ident(), state_name in valid_ident(), equalities in prop::collection::vec( (valid_ident(), prop_oneof![ valid_int().prop_map(|n| n.to_string()), valid_string().prop_map(|s| format!("\"{}\"", s)), Just("true".to_string()), Just("false".to_string()), ], valid_ident()), 2..5 ) ) { let transitions = equalities.iter() .map(|(var, val, target)| format!(" on {} is {} -> {}", var, val, target)) .collect::>() .join("\n"); let input = format!("life_arc {} {{\n state {} {{\n{}\n }}\n}}", name, state_name, transitions); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse multiple equality transitions: {:?}", result.err()); } #[test] fn test_equality_mixed_with_comparisons( name in valid_ident(), state_name in valid_ident() ) { let input = format!( "life_arc {} {{\n state {} {{\n on age > 12 -> teen\n on status is active -> active_state\n on energy < 0.3 -> tired\n on completed is true -> done\n }}\n}}", name, state_name ); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse mixed equality and comparisons: {:?}", result.err()); } // ===== Logical operator tests ===== #[test] fn test_logical_and( ident1 in valid_ident(), ident2 in valid_ident(), val1 in valid_int(), val2 in valid_int() ) { let cond = format!("{} > {} and {} < {}", ident1, val1, ident2, val2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'and' operator: {:?}", result.err()); } #[test] fn test_logical_or( ident1 in valid_ident(), ident2 in valid_ident(), val1 in valid_int(), val2 in valid_int() ) { let cond = format!("{} > {} or {} < {}", ident1, val1, ident2, val2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'or' operator: {:?}", result.err()); } #[test] fn test_logical_not_with_identifier(ident in valid_ident()) { let cond = format!("not {}", ident); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'not' operator with identifier: {:?}", result.err()); } #[test] fn test_logical_not_with_comparison( ident in valid_ident(), val in valid_int() ) { let cond = format!("not {} > {}", ident, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'not' with comparison: {:?}", result.err()); } #[test] fn test_and_with_equality( ident1 in valid_ident(), ident2 in valid_ident(), val in valid_string() ) { let cond = format!("{} is true and {} is \"{}\"", ident1, ident2, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'and' with equality: {:?}", result.err()); } #[test] fn test_or_with_equality( ident1 in valid_ident(), ident2 in valid_ident() ) { let cond = format!("{} is false or {} is true", ident1, ident2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'or' with equality: {:?}", result.err()); } #[test] fn test_chained_and( ident1 in valid_ident(), ident2 in valid_ident(), ident3 in valid_ident(), val1 in valid_int(), val2 in valid_int(), val3 in valid_int() ) { let cond = format!("{} > {} and {} < {} and {} is {}", ident1, val1, ident2, val2, ident3, val3); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse chained 'and': {:?}", result.err()); } #[test] fn test_chained_or( ident1 in valid_ident(), ident2 in valid_ident(), ident3 in valid_ident() ) { let cond = format!("{} or {} or {}", ident1, ident2, ident3); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse chained 'or': {:?}", result.err()); } #[test] fn test_mixed_and_or( ident1 in valid_ident(), ident2 in valid_ident(), ident3 in valid_ident(), val1 in valid_int(), val2 in valid_int() ) { // Tests precedence: 'and' binds tighter than 'or' // This should parse as: (ident1 > val1 and ident2 < val2) or ident3 let cond = format!("{} > {} and {} < {} or {}", ident1, val1, ident2, val2, ident3); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse mixed 'and'/'or': {:?}", result.err()); } #[test] fn test_not_not(ident in valid_ident()) { let cond = format!("not not {}", ident); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse double 'not': {:?}", result.err()); } #[test] fn test_not_with_and( ident1 in valid_ident(), ident2 in valid_ident() ) { // Tests that 'not' binds tighter than 'and' // This should parse as: (not ident1) and ident2 let cond = format!("not {} and {}", ident1, ident2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse 'not' with 'and': {:?}", result.err()); } #[test] fn test_complex_nested_logic( name in valid_ident(), state_name in valid_ident() ) { let input = format!( "life_arc {} {{\n state {} {{\n on age > 18 and status is active and energy > 0.5 -> state1\n on tired or hungry or sick -> state2\n on not ready and not completed -> state3\n on health > 50 and not sick or emergency -> state4\n }}\n}}", name, state_name ); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse complex nested logic: {:?}", result.err()); } // ===== Field access tests ===== #[test] fn test_field_access_self(field in valid_ident(), val in valid_int()) { let cond = format!("self.{} > {}", field, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse self field access: {:?}", result.err()); } #[test] fn test_field_access_other(field in valid_ident(), val in valid_int()) { let cond = format!("other.{} < {}", field, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse other field access: {:?}", result.err()); } #[test] fn test_field_access_with_equality( field in valid_ident(), val in valid_string() ) { let cond = format!("self.{} is \"{}\"", field, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access with equality: {:?}", result.err()); } #[test] fn test_field_access_with_bool(field in valid_ident()) { let cond = format!("self.{} is true", field); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access with bool: {:?}", result.err()); } #[test] fn test_nested_field_access( field1 in valid_ident(), field2 in valid_ident(), val in valid_int() ) { let cond = format!("self.{}.{} > {}", field1, field2, val); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse nested field access: {:?}", result.err()); } #[test] fn test_field_access_with_and( field1 in valid_ident(), field2 in valid_ident(), val1 in valid_int(), val2 in valid_int() ) { let cond = format!("self.{} > {} and other.{} < {}", field1, val1, field2, val2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access with 'and': {:?}", result.err()); } #[test] fn test_field_access_with_or( field1 in valid_ident(), field2 in valid_ident() ) { let cond = format!("self.{} or other.{}", field1, field2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access with 'or': {:?}", result.err()); } #[test] fn test_field_access_with_not(field in valid_ident()) { let cond = format!("not self.{}", field); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access with 'not': {:?}", result.err()); } #[test] fn test_field_access_both_sides( field1 in valid_ident(), field2 in valid_ident() ) { let cond = format!("self.{} > other.{}", field1, field2); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access on both sides: {:?}", result.err()); } #[test] fn test_field_access_vs_identifier( field in valid_ident(), ident in valid_ident() ) { let cond = format!("self.{} > {}", field, ident); let input = format!("life_arc Test {{\n state s {{\n on {} -> next\n }}\n}}", cond); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse field access vs identifier: {:?}", result.err()); } #[test] fn test_complex_field_access( name in valid_ident(), state_name in valid_ident() ) { let input = format!( "life_arc {} {{\n state {} {{\n on self.age > 18 and self.status is active -> state1\n on other.bond < 0.3 or self.energy < 0.2 -> state2\n on not self.ready and other.level > 5 -> state3\n on self.health > other.health -> state4\n }}\n}}", name, state_name ); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse complex field access: {:?}", result.err()); } } // ===== Prose block property tests ===== fn valid_prose_content() -> impl Strategy { // Prose content without --- at line start prop::string::string_regex("[^\n]*(\n[^-][^\n]*)*").unwrap() } proptest! { #[test] fn test_prose_block_roundtrip( tag in valid_ident(), content in valid_prose_content() ) { let input = format!("---{}\n{}\n---", tag, content); let lexer = Lexer::new(&input); let tokens: Vec = lexer.map(|(_, tok, _)| tok).collect(); assert_eq!(tokens.len(), 1); match &tokens[0] { Token::ProseBlock(pb) => { assert_eq!(pb.tag, tag); assert_eq!(pb.content.trim(), content.trim()); } _ => panic!("Expected ProseBlock token"), } } #[test] fn test_character_with_prose( name in valid_ident(), tag in valid_ident(), content in valid_prose_content() ) { let input = format!( "character {} {{\n {}: ---{}\n{}\n---\n}}", name, tag, tag, content ); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse character with prose:\n{}\nError: {:?}", input, result.err()); } } // ===== Edge case tests ===== #[cfg(test)] mod edge_cases { use super::*; #[test] fn test_empty_input_parses() { let lexer = Lexer::new(""); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok()); assert_eq!(result.unwrap().declarations.len(), 0); } proptest! { #[test] fn test_only_whitespace_parses(ws in "[ \t\n]{1,100}") { let lexer = Lexer::new(&ws); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok()); assert_eq!(result.unwrap().declarations.len(), 0); } #[test] fn test_only_comments_parses( n in 1usize..10, comment_content in valid_string() ) { let input = (0..n) .map(|_| format!("// {}", comment_content)) .collect::>() .join("\n"); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok()); assert_eq!(result.unwrap().declarations.len(), 0); } #[test] fn test_unicode_in_strings(s in "[^\"\\\\ ]{1,20}") { let input = format!("character Test {{ name: \"{}\" }}", s); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); // Should either parse or fail gracefully let _ = result; } #[test] fn test_nested_objects(depth in 1usize..4) { let mut input = String::from("character Test { data: "); for _ in 0..depth { input.push_str("{ inner: "); } input.push_str("42"); for _ in 0..depth { input.push_str(" }"); } input.push_str(" }"); let lexer = Lexer::new(&input); let parser = FileParser::new(); let result = parser.parse(lexer); assert!(result.is_ok(), "Failed to parse nested objects (depth {}): {}", depth, input); } } }