//! Functional tests that showcase every error type with its helpful message //! //! These tests are designed to: //! 1. Ensure every error type can be triggered //! 2. Document what causes each error //! 3. Verify that error messages are helpful and clear use std::collections::HashSet; use crate::{ resolve::{ convert::convert_file, names::NameTable, validate::{ validate_behavior_tree_actions, validate_life_arc_transitions, validate_relationship_bonds, validate_schedule_overlaps, validate_trait_ranges, }, ErrorCollector, ResolveError, }, syntax::{ ast::*, lexer::Lexer, FileParser, }, Project, }; // ===== Parse Errors ===== #[test] fn test_unexpected_token_error() { let source = r#" character Martha { age 34 } "#; // Missing colon after 'age' - should trigger UnexpectedToken let lexer = Lexer::new(source); let result = FileParser::new().parse(lexer); assert!(result.is_err(), "Should fail with unexpected token"); println!("\n=== UnexpectedToken Error ==="); if let Err(e) = result { println!("{:?}", e); } } #[test] fn test_unexpected_eof_error() { let source = r#" character Martha { age: 34 "#; // Missing closing brace - should trigger UnexpectedEof let lexer = Lexer::new(source); let result = FileParser::new().parse(lexer); assert!(result.is_err(), "Should fail with unexpected EOF"); println!("\n=== UnexpectedEof Error ==="); if let Err(e) = result { println!("{:?}", e); } } #[test] fn test_invalid_token_error() { let source = "character Martha { age: @#$ }"; // Invalid character sequence - should trigger InvalidToken let lexer = Lexer::new(source); let result = FileParser::new().parse(lexer); assert!(result.is_err(), "Should fail with invalid token"); println!("\n=== InvalidToken Error ==="); if let Err(e) = result { println!("{:?}", e); } } #[test] fn test_unclosed_prose_block_error() { let source = r#" character Martha { backstory: ---backstory This is Martha's backstory. It goes on and on... But it never closes! } "#; // Prose block never closed - should trigger UnclosedProseBlock let lexer = Lexer::new(source); let result = FileParser::new().parse(lexer); assert!(result.is_err(), "Should fail with unclosed prose block"); println!("\n=== UnclosedProseBlock Error ==="); if let Err(e) = result { println!("{:?}", e); } } // ===== Resolution Errors ===== #[test] fn test_name_not_found_error() { let file = File { declarations: vec![], }; let table = NameTable::from_file(&file).unwrap(); let result = table.lookup(&["NonExistent".to_string()]); assert!(result.is_none(), "Should not find non-existent name"); // Create the actual error let error = ResolveError::NameNotFound { name: "NonExistent".to_string(), suggestion: table.find_suggestion("NonExistent"), }; println!("\n=== NameNotFound Error ==="); println!("{:?}", error); } #[test] fn test_duplicate_definition_error() { let file = File { declarations: vec![ Declaration::Character(Character { name: "Martha".to_string(), species: None, fields: vec![], template: None, span: Span::new(0, 10), }), Declaration::Character(Character { name: "Martha".to_string(), species: None, fields: vec![], template: None, span: Span::new(20, 30), }), ], }; let result = NameTable::from_file(&file); assert!(result.is_err(), "Should fail with duplicate definition"); println!("\n=== DuplicateDefinition Error ==="); if let Err(e) = result { println!("{:?}", e); } } #[test] fn test_circular_dependency_error() { // Manually create a circular dependency error for demonstration let error = ResolveError::CircularDependency { cycle: "Template A -> Template B -> Template A".to_string(), }; println!("\n=== CircularDependency Error ==="); println!("{:?}", error); } #[test] fn test_invalid_field_access_error() { let error = ResolveError::InvalidFieldAccess { message: "Field 'nonexistent' does not exist on character 'Martha'".to_string(), }; println!("\n=== InvalidFieldAccess Error ==="); println!("{:?}", error); } #[test] fn test_type_mismatch_error() { let error = ResolveError::TypeMismatch { message: "Expected number for field 'age', but got string \"thirty\"".to_string(), }; println!("\n=== TypeMismatch Error ==="); println!("{:?}", error); } #[test] fn test_validation_error_generic() { let error = ResolveError::ValidationError { message: "Cannot append field 'age': field already exists".to_string(), help: Some("The 'append' operation is used to add new fields. Use 'set' to update existing fields.".to_string()), }; println!("\n=== ValidationError Error ==="); println!("{:?}", error); } // ===== Validation Errors ===== #[test] fn test_unknown_life_arc_state_error() { let life_arc = LifeArc { name: "Growth".to_string(), states: vec![ ArcState { name: "child".to_string(), on_enter: None, transitions: vec![Transition { to: "adult".to_string(), // 'adult' exists condition: Expr::BoolLit(true), span: Span::new(0, 10), }], span: Span::new(0, 50), }, ArcState { name: "adult".to_string(), on_enter: None, transitions: vec![Transition { to: "senior".to_string(), // 'senior' doesn't exist! condition: Expr::BoolLit(true), span: Span::new(50, 60), }], span: Span::new(50, 100), }, ], span: Span::new(0, 100), }; let mut collector = ErrorCollector::new(); validate_life_arc_transitions(&life_arc, &mut collector); assert!(collector.has_errors(), "Should fail with unknown state"); println!("\n=== UnknownLifeArcState Error ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_trait_out_of_range_error_bond() { let fields = vec![Field { name: "bond".to_string(), value: Value::Float(1.5), // Out of range! span: Span::new(0, 10), }]; let mut collector = ErrorCollector::new(); validate_trait_ranges(&fields, &mut collector); assert!( collector.has_errors(), "Should fail with out of range trait" ); println!("\n=== TraitOutOfRange Error (bond too high) ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_trait_out_of_range_error_age() { let fields = vec![Field { name: "age".to_string(), value: Value::Int(200), // Out of range! span: Span::new(0, 10), }]; let mut collector = ErrorCollector::new(); validate_trait_ranges(&fields, &mut collector); assert!(collector.has_errors(), "Should fail with out of range age"); println!("\n=== TraitOutOfRange Error (age too high) ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_trait_out_of_range_negative() { let fields = vec![Field { name: "trust".to_string(), value: Value::Float(-0.2), // Negative! span: Span::new(0, 10), }]; let mut collector = ErrorCollector::new(); validate_trait_ranges(&fields, &mut collector); assert!(collector.has_errors(), "Should fail with negative trait"); println!("\n=== TraitOutOfRange Error (negative value) ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_schedule_overlap_error() { let schedule = Schedule { name: "DailyRoutine".to_string(), blocks: vec![ ScheduleBlock { activity: "work".to_string(), start: Time { hour: 8, minute: 0, second: 0, }, end: Time { hour: 12, minute: 30, second: 0, }, fields: vec![], span: Span::new(0, 50), }, ScheduleBlock { activity: "lunch".to_string(), start: Time { hour: 12, minute: 0, // Overlaps with work! second: 0, }, end: Time { hour: 13, minute: 0, second: 0, }, fields: vec![], span: Span::new(50, 100), }, ], span: Span::new(0, 100), }; let mut collector = ErrorCollector::new(); validate_schedule_overlaps(&schedule, &mut collector); assert!(collector.has_errors(), "Should fail with schedule overlap"); println!("\n=== ScheduleOverlap Error ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_unknown_behavior_action_error() { let tree = Behavior { name: "WorkDay".to_string(), root: BehaviorNode::Action("unknown_action".to_string(), vec![]), span: Span::new(0, 50), }; // Create a registry with some known actions (but not "unknown_action") let mut action_registry = HashSet::new(); action_registry.insert("walk".to_string()); action_registry.insert("work".to_string()); action_registry.insert("eat".to_string()); let mut collector = ErrorCollector::new(); validate_behavior_tree_actions(&tree, &action_registry, &mut collector); assert!(collector.has_errors(), "Should fail with unknown action"); println!("\n=== UnknownBehaviorAction Error ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_relationship_bond_out_of_range() { let relationship = Relationship { name: "Test".to_string(), participants: vec![], fields: vec![Field { name: "bond".to_string(), value: Value::Float(2.5), // Way out of range! span: Span::new(0, 10), }], span: Span::new(0, 50), }; let mut collector = ErrorCollector::new(); validate_relationship_bonds(&[relationship], &mut collector); assert!(collector.has_errors(), "Should fail with bond out of range"); println!("\n=== Relationship Bond Out of Range ==="); if collector.has_errors() { let result = collector.into_result(()); if let Err(e) = result { println!("{:?}", e); } } } #[test] fn test_duplicate_field_in_convert() { let character = Character { name: "Martha".to_string(), species: None, fields: vec![ Field { name: "age".to_string(), value: Value::Int(34), span: Span::new(0, 10), }, Field { name: "age".to_string(), // Duplicate! value: Value::Int(35), span: Span::new(10, 20), }, ], template: None, span: Span::new(0, 50), }; let file = File { declarations: vec![Declaration::Character(character)], }; let result = convert_file(&file); assert!(result.is_err(), "Should fail with duplicate field"); println!("\n=== Duplicate Field Error (in conversion) ==="); if let Err(e) = result { println!("{:?}", e); } } // ===== Project Errors ===== #[test] fn test_invalid_project_structure_no_directory() { let result = Project::load("/nonexistent/path/to/project"); assert!(result.is_err(), "Should fail with invalid structure"); println!("\n=== InvalidStructure Error (directory doesn't exist) ==="); if let Err(e) = result { println!("{:?}", e); } } #[test] fn test_invalid_project_structure_not_directory() { // Try to load a file as if it were a directory let result = Project::load("Cargo.toml"); assert!(result.is_err(), "Should fail - file not directory"); println!("\n=== InvalidStructure Error (not a directory) ==="); if let Err(e) = result { println!("{:?}", e); } } // ===== Showcase All Errors ===== #[test] #[ignore] // Run with: cargo test error_showcase -- --ignored --nocapture fn error_showcase_all() { println!("\n\n"); println!("╔════════════════════════════════════════════════════════════════╗"); println!("║ STORYBOOK ERROR MESSAGES SHOWCASE ║"); println!("║ Every error type with helpful hints for users ║"); println!("╚════════════════════════════════════════════════════════════════╝"); test_unexpected_token_error(); test_unexpected_eof_error(); test_invalid_token_error(); test_unclosed_prose_block_error(); test_name_not_found_error(); test_duplicate_definition_error(); test_circular_dependency_error(); test_invalid_field_access_error(); test_type_mismatch_error(); test_validation_error_generic(); test_unknown_life_arc_state_error(); test_trait_out_of_range_error_bond(); test_trait_out_of_range_error_age(); test_trait_out_of_range_negative(); test_schedule_overlap_error(); test_unknown_behavior_action_error(); test_relationship_bond_out_of_range(); test_duplicate_field_in_convert(); test_invalid_project_structure_no_directory(); test_invalid_project_structure_not_directory(); println!("\n\n"); println!("╔════════════════════════════════════════════════════════════════╗"); println!("║ SHOWCASE COMPLETE ║"); println!("╚════════════════════════════════════════════════════════════════╝"); }