//! Property-based tests for the resolution engine use proptest::prelude::*; use crate::{ resolve::names::{ DeclKind, NameTable, }, syntax::ast::*, }; // ===== Generators ===== 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_character_decl() -> impl Strategy { valid_ident().prop_map(|name| { let decl = Declaration::Character(Character { name: name.clone(), species: None, fields: vec![], template: None, uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), }); (name, decl) }) } fn valid_template_decl() -> impl Strategy { valid_ident().prop_map(|name| { let decl = Declaration::Template(Template { name: name.clone(), fields: vec![], strict: false, includes: vec![], uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), }); (name, decl) }) } fn valid_enum_decl() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(name, variants)| { let decl = Declaration::Enum(EnumDecl { name: name.clone(), variants, span: Span::new(0, 10), }); (name, decl) }) } fn valid_use_single() -> impl Strategy { (valid_ident(), valid_ident()).prop_map(|(module, name)| { Declaration::Use(UseDecl { path: vec![module, name], kind: UseKind::Single, span: Span::new(0, 10), }) }) } fn valid_use_grouped() -> impl Strategy { (valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(module, names)| { Declaration::Use(UseDecl { path: vec![module], kind: UseKind::Grouped(names), span: Span::new(0, 10), }) }) } fn valid_use_wildcard() -> impl Strategy { valid_ident().prop_map(|module| { Declaration::Use(UseDecl { path: vec![module], kind: UseKind::Wildcard, span: Span::new(0, 10), }) }) } // ===== Property Tests ===== proptest! { #[test] fn test_name_table_registers_all_declarations( chars in prop::collection::vec(valid_character_decl(), 0..10) ) { let declarations: Vec<_> = chars.iter().map(|(_, decl)| decl.clone()).collect(); let file = File { declarations }; let result = NameTable::from_file(&file); if chars.is_empty() { // Empty file should succeed assert!(result.is_ok()); } else { // Check if there are duplicates let mut seen = std::collections::HashSet::new(); let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name)); if has_duplicates { // Should fail with duplicate error assert!(result.is_err()); } else { // Should succeed and all names should be registered let table = result.unwrap(); for (name, _) in &chars { assert!(table.lookup(std::slice::from_ref(name)).is_some(), "Name '{}' should be registered", name); } } } } #[test] fn test_duplicate_detection_always_fails( name in valid_ident(), count in 2usize..5 ) { let declarations: Vec<_> = (0..count).map(|i| { Declaration::Character(Character { name: name.clone(), species: None, fields: vec![], template: None, uses_behaviors: None, uses_schedule: None, span: Span::new(i * 10, i * 10 + 10), }) }).collect(); let file = File { declarations }; let result = NameTable::from_file(&file); // Should always fail with duplicate error assert!(result.is_err()); } #[test] fn test_lookup_is_case_sensitive(name in valid_ident()) { let file = File { declarations: vec![ Declaration::Character(Character { name: name.clone(), species: None, fields: vec![], template: None, uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), }), ], }; let table = NameTable::from_file(&file).unwrap(); // Original name should be found assert!(table.lookup(std::slice::from_ref(&name)).is_some()); // Different case should not be found let uppercase = name.to_uppercase(); if uppercase != name { assert!(table.lookup(&[uppercase]).is_none()); } } #[test] fn test_kind_filtering_works( chars in prop::collection::vec(valid_character_decl(), 0..5), templates in prop::collection::vec(valid_template_decl(), 0..5), enums in prop::collection::vec(valid_enum_decl(), 0..5) ) { let mut declarations = vec![]; declarations.extend(chars.iter().map(|(_, d)| d.clone())); declarations.extend(templates.iter().map(|(_, d)| d.clone())); declarations.extend(enums.iter().map(|(_, d)| d.clone())); let file = File { declarations }; // Only proceed if no duplicates let mut seen = std::collections::HashSet::new(); let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name)) || templates.iter().any(|(name, _)| !seen.insert(name)) || enums.iter().any(|(name, _)| !seen.insert(name)); if !has_duplicates { let table = NameTable::from_file(&file).unwrap(); let char_count = table.entries_of_kind(DeclKind::Character).count(); let template_count = table.entries_of_kind(DeclKind::Template).count(); let enum_count = table.entries_of_kind(DeclKind::Enum).count(); assert_eq!(char_count, chars.len()); assert_eq!(template_count, templates.len()); assert_eq!(enum_count, enums.len()); } } #[test] fn test_use_statements_are_collected( uses in prop::collection::vec( prop_oneof![ valid_use_single(), valid_use_grouped(), valid_use_wildcard(), ], 0..10 ) ) { let file = File { declarations: uses.clone() }; let table = NameTable::from_file(&file).unwrap(); assert_eq!(table.imports().len(), uses.len()); } #[test] fn test_fuzzy_matching_finds_close_names( name in valid_ident().prop_filter("long enough", |s| s.len() > 3) ) { let file = File { declarations: vec![ Declaration::Character(Character { name: name.clone(), species: None, fields: vec![], template: None, uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), }), ], }; let table = NameTable::from_file(&file).unwrap(); // Create a typo by swapping two adjacent characters if name.len() >= 2 { let mut chars: Vec = name.chars().collect(); chars.swap(0, 1); let typo: String = chars.into_iter().collect(); // Should suggest the original name if let Some(suggestion) = table.find_suggestion(&typo) { assert_eq!(suggestion, name); } } } #[test] fn test_mixed_declarations_and_imports( chars in prop::collection::vec(valid_character_decl(), 1..5), uses in prop::collection::vec(valid_use_single(), 0..3) ) { let mut declarations = uses; declarations.extend(chars.iter().map(|(_, d)| d.clone())); let file = File { declarations }; // Check for duplicates let mut seen = std::collections::HashSet::new(); let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name)); if !has_duplicates { let table = NameTable::from_file(&file).unwrap(); // All characters should be registered for (name, _) in &chars { assert!(table.lookup(std::slice::from_ref(name)).is_some()); } } } }