//! Property tests for bidirectional relationship resolution use proptest::prelude::*; use crate::{ resolve::links::resolve_relationships, syntax::ast::*, }; // ===== Generators ===== fn valid_ident() -> impl Strategy { "[a-zA-Z_][a-zA-Z0-9_]{0,15}".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_field() -> impl Strategy { (valid_ident(), 0i64..100).prop_map(|(name, value)| Field { name, value: Value::Int(value), span: Span::new(0, 10), }) } fn valid_field_list() -> impl Strategy> { prop::collection::vec(valid_field(), 0..5) // Ensure unique field names .prop_map(|fields| { let mut unique_fields = Vec::new(); let mut seen_names = std::collections::HashSet::new(); for field in fields { if seen_names.insert(field.name.clone()) { unique_fields.push(field); } } unique_fields }) } fn valid_participant(name: String) -> impl Strategy { prop::option::of(valid_ident()).prop_map(move |role| Participant { name: vec![name.clone()], role, fields: vec![], span: Span::new(0, 10), }) } #[allow(dead_code)] fn valid_participant_with_blocks(name: String) -> impl Strategy { (prop::option::of(valid_ident()), valid_field_list()).prop_map(move |(role, fields)| { Participant { name: vec![name.clone()], role, fields, span: Span::new(0, 10), } }) } fn valid_relationship() -> impl Strategy { ( valid_ident(), valid_ident(), valid_ident(), valid_field_list(), ) .prop_flat_map(|(rel_name, person1, person2, fields)| { ( Just(rel_name), valid_participant(person1.clone()), valid_participant(person2.clone()), Just(fields), ) }) .prop_map(|(name, p1, p2, fields)| Relationship { name, participants: vec![p1, p2], fields, span: Span::new(0, 10), }) } fn valid_bidirectional_relationship() -> impl Strategy { ( valid_ident(), valid_ident(), valid_ident(), valid_field_list(), valid_field_list(), ) .prop_flat_map(|(rel_name, person1, person2, shared_fields, self_fields)| { let self_fields_clone = self_fields.clone(); ( Just(rel_name.clone()), Just(person1.clone()), Just(person2.clone()), Just(shared_fields.clone()), Just(self_fields), Just(self_fields_clone), ) }) .prop_map(|(name, p1_name, p2_name, shared, p1_self, p2_self)| { // First declaration from p1's perspective let p1 = Participant { name: vec![p1_name.clone()], role: None, fields: p1_self, span: Span::new(0, 10), }; let p2_in_p1_rel = Participant { name: vec![p2_name.clone()], role: None, fields: vec![], span: Span::new(0, 10), }; let rel1 = Relationship { name: name.clone(), participants: vec![p1, p2_in_p1_rel], fields: shared.clone(), span: Span::new(0, 10), }; // Second declaration from p2's perspective let p2 = Participant { name: vec![p2_name], role: None, fields: p2_self, span: Span::new(20, 30), }; let p1_in_p2_rel = Participant { name: vec![p1_name], role: None, fields: vec![], span: Span::new(20, 30), }; let rel2 = Relationship { name, participants: vec![p2, p1_in_p2_rel], fields: shared, span: Span::new(20, 30), }; (rel1, rel2) }) } // ===== Property Tests ===== proptest! { #[test] fn test_single_relationship_always_resolves(rel in valid_relationship()) { let file = File { declarations: vec![Declaration::Relationship(rel)], }; let result = resolve_relationships(&file); assert!(result.is_ok(), "Single relationship should always resolve"); let resolved = result.unwrap(); assert_eq!(resolved.len(), 1); } #[test] fn test_relationship_participant_count_preserved(rel in valid_relationship()) { let file = File { declarations: vec![Declaration::Relationship(rel.clone())], }; let resolved = resolve_relationships(&file).unwrap(); assert_eq!(resolved[0].participants.len(), rel.participants.len()); } #[test] fn test_relationship_fields_preserved(rel in valid_relationship()) { let file = File { declarations: vec![Declaration::Relationship(rel.clone())], }; let resolved = resolve_relationships(&file).unwrap(); assert_eq!(resolved[0].fields.len(), rel.fields.len()); } #[test] fn test_bidirectional_relationships_merge( (rel1, rel2) in valid_bidirectional_relationship() ) { let file = File { declarations: vec![ Declaration::Relationship(rel1), Declaration::Relationship(rel2), ], }; let result = resolve_relationships(&file); assert!(result.is_ok(), "Bidirectional relationships should merge successfully"); let resolved = result.unwrap(); // Should merge into single relationship assert_eq!(resolved.len(), 1); } #[test] fn test_participant_order_doesnt_matter( name in valid_ident(), p1 in valid_ident(), p2 in valid_ident(), fields in valid_field_list() ) { // Create two identical relationships with participants in different order let rel1 = Relationship { name: name.clone(), participants: vec![ Participant { name: vec![p1.clone()], role: None, fields: vec![], span: Span::new(0, 10), }, Participant { name: vec![p2.clone()], role: None, fields: vec![], span: Span::new(0, 10), }, ], fields: fields.clone(), span: Span::new(0, 10), }; let rel2 = Relationship { name: name.clone(), participants: vec![ Participant { name: vec![p2.clone()], role: None, fields: vec![], span: Span::new(20, 30), }, Participant { name: vec![p1.clone()], role: None, fields: vec![], span: Span::new(20, 30), }, ], fields, span: Span::new(20, 30), }; let file = File { declarations: vec![ Declaration::Relationship(rel1), Declaration::Relationship(rel2), ], }; let result = resolve_relationships(&file); assert!(result.is_ok()); let resolved = result.unwrap(); // Should recognize as same relationship despite order assert_eq!(resolved.len(), 1); } #[test] fn test_different_relationships_stay_separate( name1 in valid_ident(), name2 in valid_ident(), p1 in valid_ident(), p2 in valid_ident() ) { // Skip if names are the same if name1 == name2 { return Ok(()); } let rel1 = Relationship { name: name1, participants: vec![ Participant { name: vec![p1.clone()], role: None, fields: vec![], span: Span::new(0, 10), }, Participant { name: vec![p2.clone()], role: None, fields: vec![], span: Span::new(0, 10), }, ], fields: vec![], span: Span::new(0, 10), }; let rel2 = Relationship { name: name2, participants: vec![ Participant { name: vec![p1], role: None, fields: vec![], span: Span::new(20, 30), }, Participant { name: vec![p2], role: None, fields: vec![], span: Span::new(20, 30), }, ], fields: vec![], span: Span::new(20, 30), }; let file = File { declarations: vec![ Declaration::Relationship(rel1), Declaration::Relationship(rel2), ], }; let result = resolve_relationships(&file); assert!(result.is_ok()); let resolved = result.unwrap(); // Different relationship names should stay separate assert_eq!(resolved.len(), 2); } #[test] fn test_self_blocks_are_merged( name in valid_ident(), p1 in valid_ident(), p2 in valid_ident(), fields1 in valid_field_list(), fields2 in valid_field_list() ) { let participant1 = Participant { name: vec![p1.clone()], role: None, fields: fields1, span: Span::new(0, 10), }; let participant1_again = Participant { name: vec![p1.clone()], role: None, fields: fields2, span: Span::new(20, 30), }; let rel1 = Relationship { name: name.clone(), participants: vec![ participant1, Participant { name: vec![p2.clone()], role: None, fields: vec![], span: Span::new(0, 10), }, ], fields: vec![], span: Span::new(0, 10), }; let rel2 = Relationship { name: name.clone(), participants: vec![ participant1_again, Participant { name: vec![p2], role: None, fields: vec![], span: Span::new(20, 30), }, ], fields: vec![], span: Span::new(20, 30), }; let file = File { declarations: vec![ Declaration::Relationship(rel1), Declaration::Relationship(rel2), ], }; let result = resolve_relationships(&file); // Should succeed unless there are conflicting field values // (which is tested separately) if result.is_ok() { let resolved = result.unwrap(); assert_eq!(resolved.len(), 1); } } #[test] fn test_empty_file_gives_empty_result( decls in prop::collection::vec( prop_oneof![ valid_ident().prop_map(|name| Declaration::Character(Character { name, species: None, fields: vec![], template: None, uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), })), valid_ident().prop_map(|name| Declaration::Template(Template { name, fields: vec![], strict: false, includes: vec![], uses_behaviors: None, uses_schedule: None, span: Span::new(0, 10), })), ], 0..5 ) ) { // File with no relationships let file = File { declarations: decls }; let result = resolve_relationships(&file); assert!(result.is_ok()); let resolved = result.unwrap(); assert_eq!(resolved.len(), 0); } }