Add syntax enhancements for more expressive behavior trees and relationships.
Action parameters:
- Support typed/positional parameters: WaitDuration(2.0)
- Support named parameters: SetValue(field: value)
- Enable inline values without field names
Repeater decorator:
- Add * { node } syntax for repeating behavior nodes
- Maps to Decorator("repeat", node)
Named participant blocks:
- Replace self/other blocks with named participant syntax
- Support multi-party relationships
- Example: Alice { role: seeker } instead of self { role: seeker }
Schedule block syntax:
- Require braces for schedule blocks to support narrative fields
- Update tests to use new syntax: 04:00 -> 06:00: Activity { }
514 lines
15 KiB
Rust
514 lines
15 KiB
Rust
//! 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!("╚════════════════════════════════════════════════════════════════╝");
|
|
}
|